iOS下子Skia控件手势捕获与父ScrollView滚动冲突的解决需求
Let's tackle this iOS scrolling issue step by step. The core problem here is that your ScrollView isn't reliably getting the signal to re-enable scrolling after interacting with the Skia ChartView, especially when touch events get cancelled. Here's how to fix it:
1. Fix ChartView's Touch Event Handling for Cancelled Actions
Your current ChartView code returns early when SKTouchAction.Cancelled is triggered, which means you never set AllowStackScroll = true in that scenario. On iOS, touch cancellations happen frequently (e.g., when the system takes over touch input), so we need to handle this case properly:
_currentTouchAction = e.ActionType; // Handle all touch end scenarios in one block if (e.ActionType == SKTouchAction.Cancelled || e.ActionType == SKTouchAction.Released || e.ActionType == SKTouchAction.Exited) { AllowStackScroll = true; this.Chart.OnTouchEnd(); this.InvalidateSurface(); ((MasterDetailPage)Application.Current.MainPage).IsGestureEnabled = true; // Re-enable menu gestures e.Handled = true; return; } ((MasterDetailPage)Application.Current.MainPage).IsGestureEnabled = false; TPVector point = new TPVector(e.Location.X, e.Location.Y); this.Chart.OnTouchBegan(point); this.InvalidateSurface(); e.Handled = true;
Why this works: Now we cover all cases where the touch interaction ends (Released, Exited, and Cancelled) and ensure AllowStackScroll is always set back to true when the user stops interacting with the chart.
2. Bind ScrollView's Scroll State Directly to ChartView's Property
Instead of relying on the ScrollView renderer's own touch timer (which can conflict with the ChartView's touch handling), let's sync the ScrollView's scrolling state directly to the ChartView's AllowStackScroll property.
First, update your TPScrollView to expose a bindable property that links to the ChartView's state:
// Add to TPScrollView class public static readonly BindableProperty AllowChildInteractionProperty = BindableProperty.Create(nameof(AllowChildInteraction), typeof(bool), typeof(TPScrollView), true); public bool AllowChildInteraction { get => (bool)GetValue(AllowChildInteractionProperty); set => SetValue(AllowChildInteractionProperty, value); }
Then bind this property in your XAML (use an inverse boolean converter to flip the logic, since AllowStackScroll = true means ScrollView should be enabled):
<local:TPScrollView AllowChildInteraction="{Binding Source={x:Reference MyChartView}, Path=AllowStackScroll, Converter={StaticResource InverseBooleanConverter}}"> <local:TPChartView x:Name="MyChartView" /> </local:TPScrollView>
3. Update iOS ScrollView Renderer to React to Bound Property
Adjust your TPScrollViewIOSRenderer to listen to the new AllowChildInteraction property instead of managing its own timer logic. This ensures the ScrollView's state is always in sync with the ChartView's touch state:
public class TPScrollViewIOSRenderer : ScrollViewRenderer { public new static void Init() { } protected override void OnElementChanged(VisualElementChangedEventArgs e) { base.OnElementChanged(e); if (e.OldElement != null) { ((TPScrollView)e.OldElement).PropertyChanged -= Element_PropertyChanged; } if (e.NewElement != null) { var scrollView = (TPScrollView)e.NewElement; scrollView.PropertyChanged += Element_PropertyChanged; ScrollEnabled = !scrollView.AllowChildInteraction; // Set initial state } } private void Element_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { var scrollView = (TPScrollView)sender; if (e.PropertyName == nameof(TPScrollView.AllowChildInteraction)) { Device.BeginInvokeOnMainThread(() => { ScrollEnabled = !scrollView.AllowChildInteraction; }); } } protected override void Dispose(bool disposing) { if (Element != null) { ((TPScrollView)Element).PropertyChanged -= Element_PropertyChanged; } base.Dispose(disposing); } }
Why this works: We're now letting the ChartView tell the ScrollView exactly when it should disable/enable scrolling, eliminating race conditions between the two components' touch detection logic.
4. Ensure ChartView Only Captures Long Presses
To match your desired behavior (child captures gestures only on long press), add a timer to the ChartView to delay touch handling until a long press is confirmed:
private System.Threading.Timer _longPressTimer; private bool _isLongPressDetected = false; // In your ChartView touch event handler: _currentTouchAction = e.ActionType; if (e.ActionType == SKTouchAction.Pressed) { ((MasterDetailPage)Application.Current.MainPage).IsGestureEnabled = false; AllowStackScroll = false; // Disable ScrollView immediately on press // Start timer to detect long press (500ms threshold) _longPressTimer = new System.Threading.Timer(_ => { Device.BeginInvokeOnMainThread(() => { _isLongPressDetected = true; TPVector point = new TPVector(e.Location.X, e.Location.Y); this.Chart.OnTouchBegan(point); this.InvalidateSurface(); }); }, null, 500, Timeout.Infinite); } if (e.ActionType == SKTouchAction.Released || e.ActionType == SKTouchAction.Exited || e.ActionType == SKTouchAction.Cancelled) { _longPressTimer?.Dispose(); AllowStackScroll = true; ((MasterDetailPage)Application.Current.MainPage).IsGestureEnabled = true; if (_isLongPressDetected) { this.Chart.OnTouchEnd(); this.InvalidateSurface(); e.Handled = true; } else { e.Handled = false; // Let ScrollView handle short taps } _isLongPressDetected = false; return; } // Only mark touch as handled if long press was detected e.Handled = _isLongPressDetected;
Why this works: Short taps are passed through to the ScrollView (keeping scrolling responsive), while long presses trigger the ChartView's interaction and disable scrolling until the touch ends.
With these changes, your iOS implementation will match Android's behavior perfectly: long press the chart to interact with it, release, and the ScrollView will immediately resume normal scrolling.
内容的提问来源于stack exchange,提问作者Nexxon

