Skip to content

Commit 7955a0c

Browse files
Resolve the bottom sheet swiping sync with scrolling
1 parent 4911662 commit 7955a0c

File tree

6 files changed

+692
-57
lines changed

6 files changed

+692
-57
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
namespace Syncfusion.Maui.Toolkit.BottomSheet
3+
{
4+
internal partial class BottomSheetBorder
5+
{
6+
/// <summary>
7+
/// Forwards the specified pointer action and touch point to the associated <see cref="SfBottomSheet"/> for gesture handling.
8+
/// </summary>
9+
/// <param name="action">The pointer action to deliver.</param>
10+
/// <param name="point">The touch point in device-independent pixels.</param>
11+
internal void ForwardToSheet(Internals.PointerActions action, Point point)
12+
{
13+
if (_bottomSheetRef?.TryGetTarget(out var bottomSheet) == true)
14+
{
15+
bottomSheet.OnHandleTouch(action, point);
16+
}
17+
}
18+
}
19+
}

maui/src/BottomSheet/BottomSheetBorder.cs

Lines changed: 129 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Syncfusion.Maui.Toolkit.BottomSheet
1111
/// <summary>
1212
/// Represents the <see cref="BottomSheetBorder"/> that defines the layout of bottom sheet.
1313
/// </summary>
14-
internal class BottomSheetBorder : SfBorder, ITouchListener
14+
internal partial class BottomSheetBorder : SfBorder, ITouchListener
1515
{
1616
#region Fields
1717

@@ -26,14 +26,34 @@ internal class BottomSheetBorder : SfBorder, ITouchListener
2626
UIView? _scrollableView;
2727

2828
/// <summary>
29-
/// Indicates whether pressed occurs inside a scrollable view.
29+
/// Determines whether the touch should be processed.
3030
/// </summary>
31-
bool _isPressed = false;
31+
bool _canProcessTouch = true;
3232

3333
/// <summary>
34-
/// Determines whether the touch should be processed.
34+
/// Represents a scrollable view under the finger (UIScrollView/UICollectionView).
3535
/// </summary>
36-
bool _canProcessTouch = true;
36+
UIScrollView? _iosScrollView;
37+
38+
/// <summary>
39+
/// Determines whether the touch started in a scrollable view.
40+
/// </summary>
41+
bool _iosInsideScrollable;
42+
43+
/// <summary>
44+
/// Determines whether we have handed off touch to the bottom sheet.
45+
/// </summary>
46+
bool _iosHandoff;
47+
48+
/// <summary>
49+
/// Stores the last Y position.
50+
/// </summary>
51+
double _iosLastY;
52+
53+
/// <summary>
54+
/// A small epsilon value to avoid jitter at scroll edges.
55+
/// </summary>
56+
nfloat _epsilon = 1.0f;
3757

3858
#endif
3959

@@ -94,6 +114,47 @@ bool IsScrollableView(UIView? view)
94114
return false;
95115
}
96116

117+
/// <summary>
118+
/// Determines if the inner UIScrollView can scroll in the direction of the finger movement.
119+
/// </summary>
120+
/// <param name="sv">The scrollable view to check.</param>
121+
/// <param name="dy">The scroll direction.</param>
122+
/// <returns>True if inner view is scrollable, otherwise false.</returns>
123+
bool CanInnerScroll(UIScrollView sv, double dy)
124+
{
125+
if (sv is not null)
126+
{
127+
// Insets (AdjustedContentInset is correct with safe area / content inset)
128+
nfloat topInset = sv.AdjustedContentInset.Top;
129+
nfloat bottomInset = sv.AdjustedContentInset.Bottom;
130+
131+
// Effective scrollable range
132+
nfloat visibleHeight = sv.Bounds.Height;
133+
nfloat contentHeight = sv.ContentSize.Height;
134+
// If content is shorter than the viewport, there's nothing to scroll.
135+
if (contentHeight <= visibleHeight - (topInset + bottomInset))
136+
{
137+
return false;
138+
}
139+
140+
nfloat minOffsetY = -topInset;
141+
nfloat maxOffsetY = (nfloat)Math.Max(0, contentHeight - visibleHeight + bottomInset);
142+
nfloat y = sv.ContentOffset.Y;
143+
144+
if (dy < 0) // finger up => scroll down
145+
{
146+
return y < (maxOffsetY - _epsilon);
147+
}
148+
149+
if (dy > 0) // finger down => scroll up
150+
{
151+
return y > (minOffsetY + _epsilon);
152+
}
153+
}
154+
155+
return false;
156+
}
157+
97158
#endif
98159

99160
#endregion
@@ -125,35 +186,77 @@ public void OnTouch(Internals.PointerEventArgs e)
125186

126187
if (IsScrollableView(hitView))
127188
{
128-
_canProcessTouch = false; // Only disable bottom sheet swipe if touch is inside scrollable view
129-
_isPressed = true;
130-
return;
131-
}
132-
else
133-
{
134-
_canProcessTouch = true; // Allow bottom sheet swipe
189+
_iosScrollView = _scrollableView as UIScrollView;
190+
_iosInsideScrollable = _iosScrollView is not null;
191+
_iosHandoff = false;
192+
_iosLastY = e.TouchPoint.Y;
193+
194+
if (_iosInsideScrollable)
195+
{
196+
// Start inside a scrollable: let it consume initially.
197+
// DO NOT forward Pressed to the sheet yet.
198+
return;
199+
}
135200
}
136201
}
137-
138202
}
139203
else if (e.Action == PointerActions.Moved)
140204
{
141-
// When moved is called multiple times, this flag helps us prevent the bottom sheet scrolling
142-
if(_isPressed)
143-
{
144-
return;
145-
}
205+
if (_iosInsideScrollable && _iosScrollView is not null)
206+
{
207+
double dy = e.TouchPoint.Y - _iosLastY;
208+
209+
// While inner can scroll in this direction, don't route to sheet.
210+
if (CanInnerScroll(_iosScrollView, dy))
211+
{
212+
_iosLastY = e.TouchPoint.Y;
213+
return; // list keeps consuming
214+
}
215+
216+
// Edge reached => hand off to sheet once
217+
if (!_iosHandoff && _bottomSheetRef?.TryGetTarget(out var bottomSheetPressed) == true)
218+
{
219+
bottomSheetPressed.OnHandleTouch(PointerActions.Pressed, e.TouchPoint);
220+
_iosHandoff = true;
221+
222+
// Route this first move to the sheet as well
223+
bottomSheetPressed.OnHandleTouch(PointerActions.Moved, e.TouchPoint);
224+
_iosLastY = e.TouchPoint.Y;
225+
return;
226+
}
227+
228+
// Already handed off => keep sending moves to sheet here; skip common forward
229+
if (_iosHandoff && _bottomSheetRef?.TryGetTarget(out var bottomSheetMoved) == true)
230+
{
231+
bottomSheetMoved.OnHandleTouch(PointerActions.Moved, e.TouchPoint);
232+
_iosLastY = e.TouchPoint.Y;
233+
return;
234+
}
235+
236+
_iosLastY = e.TouchPoint.Y;
237+
return;
238+
}
146239
}
147240
else if (e.Action == PointerActions.Released || e.Action == PointerActions.Exited || e.Action == PointerActions.Cancelled)
148241
{
149-
_canProcessTouch = true;
150-
151-
// Early return to avoid bottom sheet position update after the scroll occured in a scrollable view
152-
if(_isPressed)
153-
{
154-
_isPressed = false;
155-
return;
156-
}
242+
// If we started in a scrollable and never handed off, do not forward release to sheet
243+
if (_iosInsideScrollable && !_iosHandoff)
244+
{
245+
_iosInsideScrollable = false;
246+
_iosScrollView = null;
247+
_iosHandoff = false;
248+
return;
249+
}
250+
251+
// If we did hand off, forward release here and reset; skip common forward
252+
if (_iosHandoff && _bottomSheetRef?.TryGetTarget(out var bottomSheetReleased) == true)
253+
{
254+
bottomSheetReleased.OnHandleTouch(PointerActions.Released, e.TouchPoint);
255+
_iosInsideScrollable = false;
256+
_iosScrollView = null;
257+
_iosHandoff = false;
258+
return;
259+
}
157260
}
158261
#endif
159262

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#if ANDROID
2+
using System;
3+
using Microsoft.Maui;
4+
using Microsoft.Maui.Handlers;
5+
using Microsoft.Maui.Platform;
6+
7+
namespace Syncfusion.Maui.Toolkit.BottomSheet
8+
{
9+
/// <summary>
10+
/// Represents the <see cref="BottomSheetBorderHandler"/> Android handler, that creates its native platform view with edge-aware touch interception.
11+
/// </summary>
12+
internal class BottomSheetBorderHandler : BorderHandler
13+
{
14+
protected override ContentViewGroup CreatePlatformView()
15+
{
16+
// Correctly typed as IContentView by ContentViewHandler
17+
var content = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} must be set.");
18+
19+
if (content is not BottomSheetBorder border)
20+
{
21+
throw new InvalidOperationException(
22+
$"Expected {typeof(BottomSheetBorder).FullName}, got {content.GetType().FullName}. " +
23+
$"Verify handler registration maps BottomSheetBorder -> BottomSheetBorderHandler.");
24+
}
25+
26+
// Use our custom ContentViewGroup subclass that intercepts touch
27+
var pv = new BottomSheetBorderPlatformView(Context, border);
28+
pv.SetClipChildren(true);
29+
return pv;
30+
}
31+
32+
public override void SetVirtualView(IView view)
33+
{
34+
base.SetVirtualView(view);
35+
}
36+
}
37+
}
38+
#endif

0 commit comments

Comments
 (0)