Skip to content

Commit 1bbea6b

Browse files
authored
made sure RemoveFocusOnScroll works better (#863)
1 parent 340c269 commit 1bbea6b

15 files changed

Lines changed: 170 additions & 109 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [59.1.0]
2+
- [CollectionView] `RemoveFocusOnScroll` now uses native platform APIs instead of managed Unfocus. Listener is cleaned up on disconnect.
3+
- [ScrollView] `RemoveFocusOnScroll` now uses native platform APIs. Event subscription is removed on disconnect.
4+
15
## [59.0.0]
26
- [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.
37
- [BarcodeScanner] Added visible focused scan rectangle overlay controlled by `BarcodeScannerStartOptions.ScanRectangle` and `BarcodeScanRectangleOptions`

src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/Android/CollectionViewHandler.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ namespace DIPS.Mobile.UI.Components.Lists;
1616

1717
public partial class CollectionViewHandler
1818
{
19+
private KeyboardDismissOnScrollListener? m_keyboardDismissOnScrollListener;
20+
1921
protected override RecyclerView CreatePlatformView()
2022
{
2123
return new MauiRecyclerView(Context, GetItemsLayout, CreateAdapter);
@@ -53,6 +55,37 @@ private static partial void MapShouldBounce(CollectionViewHandler handler,
5355
collectionView.ShouldBounce ? OverScrollMode.Always : OverScrollMode.Never;
5456
}
5557
}
58+
59+
private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler,
60+
Microsoft.Maui.Controls.CollectionView virtualView)
61+
{
62+
if (virtualView is not CollectionView collectionView)
63+
return;
64+
65+
// Remove any existing listener first
66+
if (handler.m_keyboardDismissOnScrollListener != null)
67+
{
68+
handler.PlatformView.RemoveOnScrollListener(handler.m_keyboardDismissOnScrollListener);
69+
handler.m_keyboardDismissOnScrollListener = null;
70+
}
71+
72+
if (collectionView.RemoveFocusOnScroll)
73+
{
74+
handler.m_keyboardDismissOnScrollListener = new KeyboardDismissOnScrollListener();
75+
handler.PlatformView.AddOnScrollListener(handler.m_keyboardDismissOnScrollListener);
76+
}
77+
}
78+
79+
protected override void DisconnectHandler(RecyclerView platformView)
80+
{
81+
if (m_keyboardDismissOnScrollListener != null)
82+
{
83+
platformView.RemoveOnScrollListener(m_keyboardDismissOnScrollListener);
84+
m_keyboardDismissOnScrollListener = null;
85+
}
86+
87+
base.DisconnectHandler(platformView);
88+
}
5689

5790
internal partial void ReloadData(CollectionViewHandler handler)
5891
{
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Android.Views.InputMethods;
2+
using AndroidX.RecyclerView.Widget;
3+
using Microsoft.Maui.Platform;
4+
5+
namespace DIPS.Mobile.UI.Components.Lists;
6+
7+
internal class KeyboardDismissOnScrollListener : RecyclerView.OnScrollListener
8+
{
9+
public override void OnScrollStateChanged(RecyclerView recyclerView, int newState)
10+
{
11+
base.OnScrollStateChanged(recyclerView, newState);
12+
13+
if (newState != RecyclerView.ScrollStateDragging)
14+
return;
15+
16+
var context = recyclerView.Context;
17+
if (context == null)
18+
return;
19+
20+
var imm = context.GetSystemService(global::Android.Content.Context.InputMethodService) as InputMethodManager;
21+
imm?.HideSoftInputFromWindow(recyclerView.WindowToken, HideSoftInputFlags.None);
22+
}
23+
}

src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.Properties.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,22 @@ public bool HasAdditionalSpaceAtTheEnd
2727
set => SetValue(HasAdditionalSizeAtTheEndProperty, value);
2828
}
2929

30+
public static readonly BindableProperty RemoveFocusOnScrollProperty = BindableProperty.Create(
31+
nameof(RemoveFocusOnScroll),
32+
typeof(bool),
33+
typeof(CollectionView),
34+
defaultValue: false);
35+
3036
/// <summary>
31-
/// Determines if input fields should be unfocused when the user scrolls the <see cref="CollectionView"/>. (ScrollBar, Editor etc..)
37+
/// Determines if the keyboard should be dismissed when the user scrolls the <see cref="CollectionView"/>.
38+
/// On iOS this sets UIScrollView.KeyboardDismissMode to OnDrag.
39+
/// On Android this hides the soft input via InputMethodManager on scroll.
3240
/// </summary>
33-
public bool RemoveFocusOnScroll { get; set; }
41+
public bool RemoveFocusOnScroll
42+
{
43+
get => (bool)GetValue(RemoveFocusOnScrollProperty);
44+
set => SetValue(RemoveFocusOnScrollProperty, value);
45+
}
3446

3547
public static readonly BindableProperty ShouldBounceProperty = BindableProperty.Create(
3648
nameof(ShouldBounce),

src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionView.cs

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
using System.Collections;
22
using DIPS.Mobile.UI.Components.Dividers;
33
using Microsoft.Maui.Platform;
4-
using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar;
54

65
namespace DIPS.Mobile.UI.Components.Lists;
76

87
public partial class CollectionView : Microsoft.Maui.Controls.CollectionView
98
{
10-
private readonly List<WeakReference<VisualElement>> m_inputFields = [];
119
private double m_previousHeightDifference;
1210

1311
public CollectionView()
@@ -25,34 +23,6 @@ protected override void OnHandlerChanging(HandlerChangingEventArgs args)
2523
Dispose();
2624
return;
2725
}
28-
29-
30-
if (!RemoveFocusOnScroll)
31-
return;
32-
33-
var page = this.FindParentOfType<ContentPage>();
34-
RetrieveInputFields(page);
35-
}
36-
37-
private void RetrieveInputFields(IVisualTreeElement? visualTreeElement)
38-
{
39-
foreach (var child in visualTreeElement?.GetVisualTreeDescendants() ?? [])
40-
{
41-
if(Equals(child, visualTreeElement))
42-
continue;
43-
44-
switch (child)
45-
{
46-
case InputView editor:
47-
m_inputFields.Add(new WeakReference<VisualElement>(editor));
48-
break;
49-
case SearchBar searchBar:
50-
m_inputFields.Add(new WeakReference<VisualElement>(searchBar));
51-
break;
52-
}
53-
54-
RetrieveInputFields(child);
55-
}
5626
}
5727

5828
protected override void OnScrolled(ItemsViewScrolledEventArgs e)
@@ -65,7 +35,6 @@ protected override void OnScrolled(ItemsViewScrolledEventArgs e)
6535
return; //0 is idle
6636
#endif
6737
TryCollapseOrExpandElements(e);
68-
TryRemoveScroll();
6938
}
7039

7140
private void TryCollapseOrExpandElements(ItemsViewScrolledEventArgs e)
@@ -117,23 +86,6 @@ private void TryCollapseOrExpandElements(ItemsViewScrolledEventArgs e)
11786
CollapsibleElement.TrySetInputTransparent();
11887
}
11988

120-
private void TryRemoveScroll()
121-
{
122-
if (!RemoveFocusOnScroll)
123-
return;
124-
125-
foreach (var inputFieldReference in m_inputFields)
126-
{
127-
if (inputFieldReference.TryGetTarget(out var inputField))
128-
{
129-
if(inputField is SearchBar searchBar)
130-
searchBar.Unfocus();
131-
else
132-
inputField.Unfocus();
133-
}
134-
}
135-
}
136-
13789
private bool IsBouncing(ItemsViewScrolledEventArgs e)
13890
{
13991
#if __IOS__

src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/CollectionViewHandler.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ public CollectionViewHandler() : base(CollectionViewPropertyMapper)
1414

1515
public static readonly PropertyMapper CollectionViewPropertyMapper = new PropertyMapper<Microsoft.Maui.Controls.CollectionView, CollectionViewHandler>(Mapper)
1616
{
17-
[nameof(CollectionView.ShouldBounce)] = MapShouldBounce
17+
[nameof(CollectionView.ShouldBounce)] = MapShouldBounce,
18+
[nameof(CollectionView.RemoveFocusOnScroll)] = MapRemoveFocusOnScroll
1819
};
1920

2021
private static partial void MapShouldBounce(CollectionViewHandler handler, Microsoft.Maui.Controls.CollectionView virtualView);
22+
23+
private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler, Microsoft.Maui.Controls.CollectionView virtualView);
2124

2225
internal partial void ReloadData(CollectionViewHandler handler);
2326
}

src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/dotnet/CollectionViewHandler.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ private static partial void MapShouldBounce(CollectionViewHandler handler,
1010
Microsoft.Maui.Controls.CollectionView virtualView) =>
1111
throw new Only_Here_For_UnitTests();
1212

13+
private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler,
14+
Microsoft.Maui.Controls.CollectionView virtualView) =>
15+
throw new Only_Here_For_UnitTests();
16+
1317
internal partial void ReloadData(CollectionViewHandler handler) => throw new Only_Here_For_UnitTests();
1418

1519
}

src/library/DIPS.Mobile.UI/Components/Lists/CollectionView/iOS/CollectionViewHandler.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ private static partial void MapShouldBounce(CollectionViewHandler handler,
4242
}
4343
}
4444
}
45+
46+
private static partial void MapRemoveFocusOnScroll(CollectionViewHandler handler,
47+
Microsoft.Maui.Controls.CollectionView virtualView)
48+
{
49+
if (handler.PlatformView.Subviews[0] is not UICollectionView uiCollectionView)
50+
return;
51+
52+
if (virtualView is CollectionView collectionView)
53+
{
54+
uiCollectionView.KeyboardDismissMode = collectionView.RemoveFocusOnScroll
55+
? UIScrollViewKeyboardDismissMode.OnDrag
56+
: UIScrollViewKeyboardDismissMode.None;
57+
}
58+
}
4559

4660
internal partial void ReloadData(CollectionViewHandler handler)
4761
{

src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/Android/ScrollViewHandler.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using Android.Views;
2+
using Android.Views.InputMethods;
3+
using Microsoft.Maui.Platform;
4+
using AView = Android.Views.View;
25

36
namespace DIPS.Mobile.UI.Components.Lists;
47

@@ -12,4 +15,41 @@ private static partial void MapShouldBounce(ScrollViewHandler handler,
1215
handler.PlatformView.OverScrollMode = scrollView.ShouldBounce ? OverScrollMode.Always : OverScrollMode.Never;
1316
}
1417
}
18+
19+
private static partial void MapRemoveFocusOnScroll(ScrollViewHandler handler,
20+
Microsoft.Maui.Controls.ScrollView virtualView)
21+
{
22+
if (virtualView is not ScrollView scrollView)
23+
return;
24+
25+
if (scrollView.RemoveFocusOnScroll)
26+
{
27+
scrollView.Scrolled += OnScrollViewScrolledForKeyboardDismiss;
28+
}
29+
else
30+
{
31+
scrollView.Scrolled -= OnScrollViewScrolledForKeyboardDismiss;
32+
}
33+
}
34+
35+
protected override void DisconnectHandler(MauiScrollView platformView)
36+
{
37+
if (VirtualView is ScrollView scrollView)
38+
{
39+
scrollView.Scrolled -= OnScrollViewScrolledForKeyboardDismiss;
40+
}
41+
}
42+
43+
private static void OnScrollViewScrolledForKeyboardDismiss(object? sender, ScrolledEventArgs e)
44+
{
45+
if (sender is not ScrollView { Handler: ScrollViewHandler handler } scrollView)
46+
return;
47+
48+
var context = handler.PlatformView.Context;
49+
if (context == null)
50+
return;
51+
52+
var imm = context.GetSystemService(global::Android.Content.Context.InputMethodService) as InputMethodManager;
53+
imm?.HideSoftInputFromWindow(handler.PlatformView.WindowToken, HideSoftInputFlags.None);
54+
}
1555
}

src/library/DIPS.Mobile.UI/Components/Lists/ScrollView/ScrollView.Properties.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,22 @@ public bool ShouldBounce
1717
set => SetValue(ShouldBounceProperty, value);
1818
}
1919

20+
public static readonly BindableProperty RemoveFocusOnScrollProperty = BindableProperty.Create(
21+
nameof(RemoveFocusOnScroll),
22+
typeof(bool),
23+
typeof(ScrollView),
24+
defaultValue: false);
25+
2026
/// <summary>
21-
/// Determines if input fields should be unfocused when the user scrolls the <see cref="ScrollView"/>. (ScrollBar, Editor etc..)
27+
/// Determines if the keyboard should be dismissed when the user scrolls the <see cref="ScrollView"/>.
28+
/// On iOS this sets UIScrollView.KeyboardDismissMode to OnDrag.
29+
/// On Android this hides the soft input via InputMethodManager on scroll.
2230
/// </summary>
23-
public bool RemoveFocusOnScroll { get; set; }
31+
public bool RemoveFocusOnScroll
32+
{
33+
get => (bool)GetValue(RemoveFocusOnScrollProperty);
34+
set => SetValue(RemoveFocusOnScrollProperty, value);
35+
}
2436

2537
public static readonly BindableProperty HasAdditionalSpaceAtTheEndProperty = BindableProperty.Create(
2638
nameof(HasAdditionalSpaceAtTheEnd),

0 commit comments

Comments
 (0)