From 981a535c9102cd2480cda4a83971fbe4cd121bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Wed, 13 May 2026 22:52:25 +0200 Subject: [PATCH 1/3] made sure RemoveFocusOnScroll works better --- CHANGELOG.md | 4 ++ .../Lists/CollectionViewSamples.xaml | 33 ++++++++++++ .../Lists/CollectionViewSamples.xaml.cs | 17 ++++++ .../ComponentsSamples/Lists/ListsSamples.xaml | 15 ++++++ .../Lists/ListsSamples.xaml.cs | 20 +++++++ .../Lists/ScrollViewSamples.xaml | 54 +++++++++++++++++++ .../Lists/ScrollViewSamples.xaml.cs | 9 ++++ .../Components/REGISTER_YOUR_SAMPLES_HERE.cs | 2 + .../Android/CollectionViewHandler.cs | 33 ++++++++++++ .../KeyboardDismissOnScrollListener.cs | 23 ++++++++ .../CollectionView.Properties.cs | 16 +++++- .../Lists/CollectionView/CollectionView.cs | 48 ----------------- .../CollectionView/CollectionViewHandler.cs | 5 +- .../dotnet/CollectionViewHandler.cs | 4 ++ .../iOS/CollectionViewHandler.cs | 14 +++++ .../ScrollView/Android/ScrollViewHandler.cs | 40 ++++++++++++++ .../Lists/ScrollView/ScrollView.Properties.cs | 16 +++++- .../Components/Lists/ScrollView/ScrollView.cs | 54 ------------------- .../Lists/ScrollView/ScrollViewHandler.cs | 5 +- .../ScrollView/dotnet/ScrollViewHandler.cs | 4 ++ .../Lists/ScrollView/iOS/ScrollViewHandler.cs | 11 ++++ wiki/Components/CollectionView.md | 2 +- 22 files changed, 320 insertions(+), 109 deletions(-) create mode 100644 src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml create mode 100644 src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs create mode 100644 src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml create mode 100644 src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs create mode 100644 src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml create mode 100644 src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d81a12e..b91e50e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [59.1.0] +- [CollectionView] `RemoveFocusOnScroll` bruker nå native plattform-API-er (iOS `KeyboardDismissMode`, Android `OnScrollListener`) i stedet for managed Unfocus. Listener ryddes opp ved disconnect for å unngå minnelekkasjer. +- [ScrollView] `RemoveFocusOnScroll` bruker nå native plattform-API-er (iOS `KeyboardDismissMode`, Android `Scrolled`-event med `InputMethodManager`). Event-abonnement fjernes ved disconnect. + ## [59.0.0] - [BarcodeScanner] **BREAKING**: Replaced positional `Start` parameters and `BarcodeScanningSettings` with `BarcodeScanner.Start(BarcodeScannerStartOptions)` so preview, camera failure handling, validation, async callbacks, scan rectangle, and completion behavior are configured in one scanner session contract. - [BarcodeScanner] Added visible focused scan rectangle overlay controlled by `BarcodeScannerStartOptions.ScanRectangle` and `BarcodeScanRectangleOptions` diff --git a/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml b/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml new file mode 100644 index 000000000..9770584b2 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs b/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs new file mode 100644 index 000000000..bce1d7004 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; + +namespace Components.ComponentsSamples.Lists; + +public partial class CollectionViewSamples +{ + public CollectionViewSamples() + { + Items = new ObservableCollection( + Enumerable.Range(1, 50).Select(i => $"Item {i}")); + + BindingContext = this; + InitializeComponent(); + } + + public ObservableCollection Items { get; } +} diff --git a/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml b/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml new file mode 100644 index 000000000..d70ed5fd0 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs b/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs new file mode 100644 index 000000000..205b97c6e --- /dev/null +++ b/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs @@ -0,0 +1,20 @@ +using System.Windows.Input; + +namespace Components.ComponentsSamples.Lists; + +public partial class ListsSamples +{ + public ListsSamples() + { + NavigateToCollectionViewCommand = new Command(async () => + await Shell.Current.Navigation.PushAsync(new CollectionViewSamples())); + NavigateToScrollViewCommand = new Command(async () => + await Shell.Current.Navigation.PushAsync(new ScrollViewSamples())); + + BindingContext = this; + InitializeComponent(); + } + + public ICommand NavigateToCollectionViewCommand { get; } + public ICommand NavigateToScrollViewCommand { get; } +} diff --git a/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml b/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml new file mode 100644 index 000000000..23b2b3bc1 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs b/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs new file mode 100644 index 000000000..8db298567 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs @@ -0,0 +1,9 @@ +namespace Components.ComponentsSamples.Lists; + +public partial class ScrollViewSamples +{ + public ScrollViewSamples() + { + InitializeComponent(); + } +} diff --git a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs index a709e9a44..d692313b4 100644 --- a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs +++ b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs @@ -11,6 +11,7 @@ using Components.ComponentsSamples.ImageCapturing; using Components.ComponentsSamples.Labels; using Components.ComponentsSamples.ListItems; +using Components.ComponentsSamples.Lists; using Components.ComponentsSamples.Loading; using Components.ComponentsSamples.Navigation; using Components.ComponentsSamples.PanZoomContainer; @@ -78,6 +79,7 @@ public static List RegisterSamples() new(SampleType.Components, "TIFF Viewer", () => new TiffViewerSample()), new(SampleType.Components, "Toolbar", () => new ToolbarSamples(), isModal: true), new(SampleType.Accessibility, "VoiceOver/TalkBack", () => new VoiceOverSamples()), + new(SampleType.Components, "Lists", () => new ListsSamples()), diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/CollectionViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/CollectionViewHandler.cs index 4f3e41ac8..166fe16be 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/CollectionViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/CollectionViewHandler.cs @@ -16,6 +16,8 @@ namespace DIPS.Mobile.UI.Components.Lists; public partial class CollectionViewHandler { + private KeyboardDismissOnScrollListener? m_keyboardDismissOnScrollListener; + protected override RecyclerView CreatePlatformView() { return new MauiRecyclerView(Context, GetItemsLayout, CreateAdapter); @@ -53,6 +55,37 @@ private static partial void MapShouldBounce(CollectionViewHandler handler, collectionView.ShouldBounce ? OverScrollMode.Always : OverScrollMode.Never; } } + + private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler, + Microsoft.Maui.Controls.CollectionView virtualView) + { + if (virtualView is not CollectionView collectionView) + return; + + // Remove any existing listener first + if (handler.m_keyboardDismissOnScrollListener != null) + { + handler.PlatformView.RemoveOnScrollListener(handler.m_keyboardDismissOnScrollListener); + handler.m_keyboardDismissOnScrollListener = null; + } + + if (collectionView.RemoveFocusOnScroll) + { + handler.m_keyboardDismissOnScrollListener = new KeyboardDismissOnScrollListener(); + handler.PlatformView.AddOnScrollListener(handler.m_keyboardDismissOnScrollListener); + } + } + + protected override void DisconnectHandler(RecyclerView platformView) + { + if (m_keyboardDismissOnScrollListener != null) + { + platformView.RemoveOnScrollListener(m_keyboardDismissOnScrollListener); + m_keyboardDismissOnScrollListener = null; + } + + base.DisconnectHandler(platformView); + } internal partial void ReloadData(CollectionViewHandler handler) { diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs new file mode 100644 index 000000000..ce4a10ce8 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs @@ -0,0 +1,23 @@ +using Android.Views.InputMethods; +using AndroidX.RecyclerView.Widget; +using Microsoft.Maui.Platform; + +namespace DIPS.Mobile.UI.Components.Lists; + +internal class KeyboardDismissOnScrollListener : RecyclerView.OnScrollListener +{ + public override void OnScrollStateChanged(RecyclerView recyclerView, int newState) + { + base.OnScrollStateChanged(recyclerView, newState); + + if (newState != RecyclerView.ScrollStateDragging) + return; + + var context = recyclerView.Context; + if (context == null) + return; + + var imm = (InputMethodManager?)context.GetSystemService(global::Android.Content.Context.InputMethodService); + imm?.HideSoftInputFromWindow(recyclerView.WindowToken, HideSoftInputFlags.None); + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.Properties.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.Properties.cs index 2790fe310..f7cdeec49 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.Properties.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.Properties.cs @@ -27,10 +27,22 @@ public bool HasAdditionalSpaceAtTheEnd set => SetValue(HasAdditionalSizeAtTheEndProperty, value); } + public static readonly BindableProperty RemoveFocusOnScrollProperty = BindableProperty.Create( + nameof(RemoveFocusOnScroll), + typeof(bool), + typeof(CollectionView), + defaultValue: false); + /// - /// Determines if input fields should be unfocused when the user scrolls the . (ScrollBar, Editor etc..) + /// Determines if the keyboard should be dismissed when the user scrolls the . + /// On iOS this sets UIScrollView.KeyboardDismissMode to OnDrag. + /// On Android this hides the soft input via InputMethodManager on scroll. /// - public bool RemoveFocusOnScroll { get; set; } + public bool RemoveFocusOnScroll + { + get => (bool)GetValue(RemoveFocusOnScrollProperty); + set => SetValue(RemoveFocusOnScrollProperty, value); + } public static readonly BindableProperty ShouldBounceProperty = BindableProperty.Create( nameof(ShouldBounce), diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.cs index 132f15b10..245457eb7 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.cs @@ -1,13 +1,11 @@ using System.Collections; using DIPS.Mobile.UI.Components.Dividers; using Microsoft.Maui.Platform; -using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar; namespace DIPS.Mobile.UI.Components.Lists; public partial class CollectionView : Microsoft.Maui.Controls.CollectionView { - private readonly List> m_inputFields = []; private double m_previousHeightDifference; public CollectionView() @@ -25,34 +23,6 @@ protected override void OnHandlerChanging(HandlerChangingEventArgs args) Dispose(); return; } - - - if (!RemoveFocusOnScroll) - return; - - var page = this.FindParentOfType(); - RetrieveInputFields(page); - } - - private void RetrieveInputFields(IVisualTreeElement? visualTreeElement) - { - foreach (var child in visualTreeElement?.GetVisualTreeDescendants() ?? []) - { - if(Equals(child, visualTreeElement)) - continue; - - switch (child) - { - case InputView editor: - m_inputFields.Add(new WeakReference(editor)); - break; - case SearchBar searchBar: - m_inputFields.Add(new WeakReference(searchBar)); - break; - } - - RetrieveInputFields(child); - } } protected override void OnScrolled(ItemsViewScrolledEventArgs e) @@ -65,7 +35,6 @@ protected override void OnScrolled(ItemsViewScrolledEventArgs e) return; //0 is idle #endif TryCollapseOrExpandElements(e); - TryRemoveScroll(); } private void TryCollapseOrExpandElements(ItemsViewScrolledEventArgs e) @@ -117,23 +86,6 @@ private void TryCollapseOrExpandElements(ItemsViewScrolledEventArgs e) CollapsibleElement.TrySetInputTransparent(); } - private void TryRemoveScroll() - { - if (!RemoveFocusOnScroll) - return; - - foreach (var inputFieldReference in m_inputFields) - { - if (inputFieldReference.TryGetTarget(out var inputField)) - { - if(inputField is SearchBar searchBar) - searchBar.Unfocus(); - else - inputField.Unfocus(); - } - } - } - private bool IsBouncing(ItemsViewScrolledEventArgs e) { #if __IOS__ diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionViewHandler.cs index ccba74c8f..5d6710c4a 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionViewHandler.cs @@ -14,10 +14,13 @@ public CollectionViewHandler() : base(CollectionViewPropertyMapper) public static readonly PropertyMapper CollectionViewPropertyMapper = new PropertyMapper(Mapper) { - [nameof(CollectionView.ShouldBounce)] = MapShouldBounce + [nameof(CollectionView.ShouldBounce)] = MapShouldBounce, + [nameof(CollectionView.RemoveFocusOnScroll)] = MapRemoveFocusOnScroll }; private static partial void MapShouldBounce(CollectionViewHandler handler, Microsoft.Maui.Controls.CollectionView virtualView); + + private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler, Microsoft.Maui.Controls.CollectionView virtualView); internal partial void ReloadData(CollectionViewHandler handler); } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/dotnet/CollectionViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/dotnet/CollectionViewHandler.cs index b00c88c42..f02fbebf6 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/dotnet/CollectionViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/dotnet/CollectionViewHandler.cs @@ -10,6 +10,10 @@ private static partial void MapShouldBounce(CollectionViewHandler handler, Microsoft.Maui.Controls.CollectionView virtualView) => throw new Only_Here_For_UnitTests(); + private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler, + Microsoft.Maui.Controls.CollectionView virtualView) => + throw new Only_Here_For_UnitTests(); + internal partial void ReloadData(CollectionViewHandler handler) => throw new Only_Here_For_UnitTests(); } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/iOS/CollectionViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/iOS/CollectionViewHandler.cs index 70952fbca..b65b4be83 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/iOS/CollectionViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/iOS/CollectionViewHandler.cs @@ -42,6 +42,20 @@ private static partial void MapShouldBounce(CollectionViewHandler handler, } } } + + private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler, + Microsoft.Maui.Controls.CollectionView virtualView) + { + if (handler.PlatformView.Subviews[0] is not UICollectionView uiCollectionView) + return; + + if (virtualView is CollectionView collectionView) + { + uiCollectionView.KeyboardDismissMode = collectionView.RemoveFocusOnScroll + ? UIScrollViewKeyboardDismissMode.OnDrag + : UIScrollViewKeyboardDismissMode.None; + } + } internal partial void ReloadData(CollectionViewHandler handler) { diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs index f090341f7..40bd9a300 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs @@ -1,4 +1,7 @@ using Android.Views; +using Android.Views.InputMethods; +using Microsoft.Maui.Platform; +using AView = Android.Views.View; namespace DIPS.Mobile.UI.Components.Lists; @@ -12,4 +15,41 @@ private static partial void MapShouldBounce(ScrollViewHandler handler, handler.PlatformView.OverScrollMode = scrollView.ShouldBounce ? OverScrollMode.Always : OverScrollMode.Never; } } + + private static partial void MapRemoveFocusOnScroll(ScrollViewHandler handler, + Microsoft.Maui.Controls.ScrollView virtualView) + { + if (virtualView is not ScrollView scrollView) + return; + + if (scrollView.RemoveFocusOnScroll) + { + scrollView.Scrolled += OnScrollViewScrolledForKeyboardDismiss; + } + else + { + scrollView.Scrolled -= OnScrollViewScrolledForKeyboardDismiss; + } + } + + protected override void DisconnectHandler(MauiScrollView platformView) + { + if (VirtualView is ScrollView scrollView) + { + scrollView.Scrolled -= OnScrollViewScrolledForKeyboardDismiss; + } + } + + private static void OnScrollViewScrolledForKeyboardDismiss(object? sender, ScrolledEventArgs e) + { + if (sender is not ScrollView { Handler: ScrollViewHandler handler } scrollView) + return; + + var context = handler.PlatformView.Context; + if (context == null) + return; + + var imm = (InputMethodManager?)context.GetSystemService(global::Android.Content.Context.InputMethodService); + imm?.HideSoftInputFromWindow(handler.PlatformView.WindowToken, HideSoftInputFlags.None); + } } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.Properties.cs b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.Properties.cs index 14a2b6815..8a5eeffe8 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.Properties.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.Properties.cs @@ -17,10 +17,22 @@ public bool ShouldBounce set => SetValue(ShouldBounceProperty, value); } + public static readonly BindableProperty RemoveFocusOnScrollProperty = BindableProperty.Create( + nameof(RemoveFocusOnScroll), + typeof(bool), + typeof(ScrollView), + defaultValue: false); + /// - /// Determines if input fields should be unfocused when the user scrolls the . (ScrollBar, Editor etc..) + /// Determines if the keyboard should be dismissed when the user scrolls the . + /// On iOS this sets UIScrollView.KeyboardDismissMode to OnDrag. + /// On Android this hides the soft input via InputMethodManager on scroll. /// - public bool RemoveFocusOnScroll { get; set; } + public bool RemoveFocusOnScroll + { + get => (bool)GetValue(RemoveFocusOnScrollProperty); + set => SetValue(RemoveFocusOnScrollProperty, value); + } public static readonly BindableProperty HasAdditionalSpaceAtTheEndProperty = BindableProperty.Create( nameof(HasAdditionalSpaceAtTheEnd), diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.cs b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.cs index 15d6b629c..7d74a2740 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.cs @@ -3,16 +3,12 @@ namespace DIPS.Mobile.UI.Components.Lists; public partial class ScrollView : Microsoft.Maui.Controls.ScrollView { private bool m_hasAddedSpaceToBottom; - - private readonly List> m_inputFields = []; public ScrollView() { #if __ANDROID__ //Not possible to set padding on scroll view after its rendered AdjustPadding(AndroidAdditionalSpaceAtEnd); #endif - - this.Scrolled += OnScrolled; } #if __IOS__ @@ -26,56 +22,6 @@ protected override void OnSizeAllocated(double width, double height) protected override void OnHandlerChanging(HandlerChangingEventArgs args) { base.OnHandlerChanging(args); - - if (args.NewHandler is null) - { - this.Scrolled -= OnScrolled; - return; - } - - if(!RemoveFocusOnScroll) - return; - - var page = this.FindParentOfType(); - RetrieveInputFields(page); - } - - private void RetrieveInputFields(IVisualTreeElement? visualTreeElement) - { - foreach (var child in visualTreeElement?.GetVisualTreeDescendants() ?? []) - { - if(Equals(child, visualTreeElement)) - continue; - - switch (child) - { - case InputView editor: - m_inputFields.Add(new WeakReference(editor)); - break; - case Searching.SearchBar searchBar: - m_inputFields.Add(new WeakReference(searchBar)); - break; - } - - RetrieveInputFields(child); - } - } - - private void OnScrolled(object? sender, ScrolledEventArgs e) - { - if (!RemoveFocusOnScroll) - return; - - foreach (var inputFieldReference in m_inputFields) - { - if (inputFieldReference.TryGetTarget(out var inputField)) - { - if(inputField is Searching.SearchBar searchBar) - searchBar.Unfocus(); - else - inputField.Unfocus(); - } - } } private void AdjustPadding(double height) diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollViewHandler.cs index f0d8b595f..33353998c 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollViewHandler.cs @@ -8,8 +8,11 @@ public ScrollViewHandler() : base(ScrollViewPropertyMapper) public static readonly PropertyMapper ScrollViewPropertyMapper = new PropertyMapper(Mapper) { - [nameof(CollectionView.ShouldBounce)] = MapShouldBounce + [nameof(CollectionView.ShouldBounce)] = MapShouldBounce, + [nameof(ScrollView.RemoveFocusOnScroll)] = MapRemoveFocusOnScroll }; private static partial void MapShouldBounce(ScrollViewHandler handler, Microsoft.Maui.Controls.ScrollView virtualView); + + private static partial void MapRemoveFocusOnScroll(ScrollViewHandler handler, Microsoft.Maui.Controls.ScrollView virtualView); } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/dotnet/ScrollViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/dotnet/ScrollViewHandler.cs index e24d22852..91331bc31 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/dotnet/ScrollViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/dotnet/ScrollViewHandler.cs @@ -7,4 +7,8 @@ public partial class ScrollViewHandler private static partial void MapShouldBounce(ScrollViewHandler handler, Microsoft.Maui.Controls.ScrollView virtualView) => throw new Only_Here_For_UnitTests(); + + private static partial void MapRemoveFocusOnScroll(ScrollViewHandler handler, + Microsoft.Maui.Controls.ScrollView virtualView) => + throw new Only_Here_For_UnitTests(); } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/iOS/ScrollViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/iOS/ScrollViewHandler.cs index 61b52aa74..1bd708eb7 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/iOS/ScrollViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/iOS/ScrollViewHandler.cs @@ -15,4 +15,15 @@ private static partial void MapShouldBounce(ScrollViewHandler handler, } } } + + private static partial void MapRemoveFocusOnScroll(ScrollViewHandler handler, + Microsoft.Maui.Controls.ScrollView virtualView) + { + if (handler.PlatformView is { } uiScrollView && virtualView is ScrollView scrollView) + { + uiScrollView.KeyboardDismissMode = scrollView.RemoveFocusOnScroll + ? UIScrollViewKeyboardDismissMode.OnDrag + : UIScrollViewKeyboardDismissMode.None; + } + } } \ No newline at end of file diff --git a/wiki/Components/CollectionView.md b/wiki/Components/CollectionView.md index b1ea5e3a1..31f2e88ef 100644 --- a/wiki/Components/CollectionView.md +++ b/wiki/Components/CollectionView.md @@ -21,7 +21,7 @@ In our CollectionView this setting is enabled by default. To turn off this behav ``` ## Losing focus on Input fields if the list is scrolled -We have implemented a property to ensure that input views lose focus when the list is scrolled. By losing focus on scroll, it prevents the keyboard hiding the CollectionView's items. +We have implemented a property to ensure that input views lose focus when the list is scrolled. By losing focus on scroll, it prevents the keyboard from hiding the CollectionView's items. To enable this feature, set the `RemoveFocusOnScroll` property to true: From edf31c08e1fd76314bef40fa69df89cff88555be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Wed, 13 May 2026 22:56:14 +0200 Subject: [PATCH 2/3] cleaned up list and changelog --- CHANGELOG.md | 4 +- .../Lists/CollectionViewSamples.xaml | 33 ------------ .../Lists/CollectionViewSamples.xaml.cs | 17 ------ .../ComponentsSamples/Lists/ListsSamples.xaml | 15 ------ .../Lists/ListsSamples.xaml.cs | 20 ------- .../Lists/ScrollViewSamples.xaml | 54 ------------------- .../Lists/ScrollViewSamples.xaml.cs | 9 ---- .../Components/REGISTER_YOUR_SAMPLES_HERE.cs | 2 - 8 files changed, 2 insertions(+), 152 deletions(-) delete mode 100644 src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml delete mode 100644 src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs delete mode 100644 src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml delete mode 100644 src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs delete mode 100644 src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml delete mode 100644 src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b91e50e55..09638c3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [59.1.0] -- [CollectionView] `RemoveFocusOnScroll` bruker nå native plattform-API-er (iOS `KeyboardDismissMode`, Android `OnScrollListener`) i stedet for managed Unfocus. Listener ryddes opp ved disconnect for å unngå minnelekkasjer. -- [ScrollView] `RemoveFocusOnScroll` bruker nå native plattform-API-er (iOS `KeyboardDismissMode`, Android `Scrolled`-event med `InputMethodManager`). Event-abonnement fjernes ved disconnect. +- [CollectionView] `RemoveFocusOnScroll` now uses native platform APIs instead of managed Unfocus. Listener is cleaned up on disconnect. +- [ScrollView] `RemoveFocusOnScroll` now uses native platform APIs. Event subscription is removed on disconnect. ## [59.0.0] - [BarcodeScanner] **BREAKING**: Replaced positional `Start` parameters and `BarcodeScanningSettings` with `BarcodeScanner.Start(BarcodeScannerStartOptions)` so preview, camera failure handling, validation, async callbacks, scan rectangle, and completion behavior are configured in one scanner session contract. diff --git a/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml b/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml deleted file mode 100644 index 9770584b2..000000000 --- a/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs b/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs deleted file mode 100644 index bce1d7004..000000000 --- a/src/app/Components/ComponentsSamples/Lists/CollectionViewSamples.xaml.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Components.ComponentsSamples.Lists; - -public partial class CollectionViewSamples -{ - public CollectionViewSamples() - { - Items = new ObservableCollection( - Enumerable.Range(1, 50).Select(i => $"Item {i}")); - - BindingContext = this; - InitializeComponent(); - } - - public ObservableCollection Items { get; } -} diff --git a/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml b/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml deleted file mode 100644 index d70ed5fd0..000000000 --- a/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs b/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs deleted file mode 100644 index 205b97c6e..000000000 --- a/src/app/Components/ComponentsSamples/Lists/ListsSamples.xaml.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Windows.Input; - -namespace Components.ComponentsSamples.Lists; - -public partial class ListsSamples -{ - public ListsSamples() - { - NavigateToCollectionViewCommand = new Command(async () => - await Shell.Current.Navigation.PushAsync(new CollectionViewSamples())); - NavigateToScrollViewCommand = new Command(async () => - await Shell.Current.Navigation.PushAsync(new ScrollViewSamples())); - - BindingContext = this; - InitializeComponent(); - } - - public ICommand NavigateToCollectionViewCommand { get; } - public ICommand NavigateToScrollViewCommand { get; } -} diff --git a/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml b/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml deleted file mode 100644 index 23b2b3bc1..000000000 --- a/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs b/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs deleted file mode 100644 index 8db298567..000000000 --- a/src/app/Components/ComponentsSamples/Lists/ScrollViewSamples.xaml.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Components.ComponentsSamples.Lists; - -public partial class ScrollViewSamples -{ - public ScrollViewSamples() - { - InitializeComponent(); - } -} diff --git a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs index d692313b4..a709e9a44 100644 --- a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs +++ b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs @@ -11,7 +11,6 @@ using Components.ComponentsSamples.ImageCapturing; using Components.ComponentsSamples.Labels; using Components.ComponentsSamples.ListItems; -using Components.ComponentsSamples.Lists; using Components.ComponentsSamples.Loading; using Components.ComponentsSamples.Navigation; using Components.ComponentsSamples.PanZoomContainer; @@ -79,7 +78,6 @@ public static List RegisterSamples() new(SampleType.Components, "TIFF Viewer", () => new TiffViewerSample()), new(SampleType.Components, "Toolbar", () => new ToolbarSamples(), isModal: true), new(SampleType.Accessibility, "VoiceOver/TalkBack", () => new VoiceOverSamples()), - new(SampleType.Components, "Lists", () => new ListsSamples()), From 384046b3464047ffd42f08444934d3a5ee4ec185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Fri, 15 May 2026 13:08:16 +0200 Subject: [PATCH 3/3] added safecast --- .../CollectionView/Android/KeyboardDismissOnScrollListener.cs | 2 +- .../Components/Lists/ScrollView/Android/ScrollViewHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs index ce4a10ce8..148903757 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/KeyboardDismissOnScrollListener.cs @@ -17,7 +17,7 @@ public override void OnScrollStateChanged(RecyclerView recyclerView, int newStat if (context == null) return; - var imm = (InputMethodManager?)context.GetSystemService(global::Android.Content.Context.InputMethodService); + var imm = context.GetSystemService(global::Android.Content.Context.InputMethodService) as InputMethodManager; imm?.HideSoftInputFromWindow(recyclerView.WindowToken, HideSoftInputFlags.None); } } diff --git a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs index 40bd9a300..22c896bb0 100644 --- a/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs @@ -49,7 +49,7 @@ private static void OnScrollViewScrolledForKeyboardDismiss(object? sender, Scrol if (context == null) return; - var imm = (InputMethodManager?)context.GetSystemService(global::Android.Content.Context.InputMethodService); + var imm = context.GetSystemService(global::Android.Content.Context.InputMethodService) as InputMethodManager; imm?.HideSoftInputFromWindow(handler.PlatformView.WindowToken, HideSoftInputFlags.None); } } \ No newline at end of file