diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36d81a12e..09638c3b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## [59.1.0]
+- [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.
- [BarcodeScanner] Added visible focused scan rectangle overlay controlled by `BarcodeScannerStartOptions.ScanRectangle` and `BarcodeScanRectangleOptions`
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..148903757
--- /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 = 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/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..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
@@ -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 = context.GetSystemService(global::Android.Content.Context.InputMethodService) as InputMethodManager;
+ 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: