Add LongPressGestureRecognizer for .NET MAUI 11#33432
Add LongPressGestureRecognizer for .NET MAUI 11#33432jfversluis wants to merge 27 commits intonet11.0from
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive LongPressGestureRecognizer for .NET MAUI 11, adding long-press gesture support across all five platforms (iOS, MacCatalyst, Android, Windows, Tizen). The implementation includes:
- A new
LongPressGestureRecognizerclass with 5 bindable properties, 2 events, and Command support - Platform-specific implementations leveraging native APIs where available
- 17 unit tests and 9 performance tests ensuring reliability and no memory leaks
- 4 UI interaction tests validating gesture coexistence
- Comprehensive XML documentation and sample gallery page
Key achievements:
- Proper gesture coexistence with Tap, Swipe, Pan, and ScrollView gestures
- Platform-specific optimizations (native UILongPressGestureRecognizer on iOS/MacCatalyst, native LongPressGestureDetector on Tizen)
- Documented platform limitations (Android's fixed duration, Windows/Android single-touch only)
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
LongPressGestureRecognizer.cs |
Core gesture recognizer with 5 properties, 2 events, state management |
LongPressedEventArgs.cs |
Event args for gesture completion with position and parameter |
LongPressingEventArgs.cs |
Event args for real-time state updates |
GesturePlatformManager.iOS.cs |
iOS/MacCatalyst native implementation with UILongPressGestureRecognizer |
LongPressGestureHandler.cs (Android) |
Android implementation using GestureDetector.OnLongPress |
InnerGestureListener.cs |
Android gesture listener integration |
GesturePlatformManager.Android.cs |
Android gesture manager integration |
LongPressGestureHandler.Windows.cs |
Windows custom timer-based implementation |
GesturePlatformManager.Windows.cs |
Windows gesture manager integration |
LongPressGestureHandler.cs (Tizen) |
Tizen native NUI LongPressGestureDetector implementation |
GestureDetector.cs (Tizen) |
Tizen gesture detector registration |
LongPressGestureRecognizerTests.cs |
17 unit tests covering all API surface |
LongPressGestureRecognizerPerformanceTests.cs |
9 performance tests validating no memory leaks |
LongPressGestureInteraction.* |
UI test page and tests for gesture coexistence (3 files) |
PublicAPI.Unshipped.txt |
API surface additions for all 7 platforms |
LongPressGestureGalleryPage.cs |
Sample gallery demonstrating 5 interactive scenarios (has bugs) |
GesturesViewModel.cs |
Gallery navigation update |
LongPressGestureRecognizer_Implementation_Plan.md |
Comprehensive implementation plan document |
src/Controls/samples/Controls.Sample/Pages/Core/LongPressGestureGalleryPage.cs
Outdated
Show resolved
Hide resolved
src/Controls/samples/Controls.Sample/Pages/Core/LongPressGestureGalleryPage.cs
Show resolved
Hide resolved
src/Controls/tests/TestCases.HostApp/Issues/LongPressGestureInteraction.xaml
Outdated
Show resolved
Hide resolved
src/Controls/tests/TestCases.HostApp/Issues/LongPressGestureInteraction.xaml.cs
Outdated
Show resolved
Hide resolved
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/LongPressGestureInteraction.cs
Show resolved
Hide resolved
pictos
left a comment
There was a problem hiding this comment.
Really great to see this coming to .net 11, I've a couple of suggestions.
The "invert if" suggestions are to reduce the cyclomatic complexity, right now it should be fine, but we don't how much this code can increase in the future
src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs
Outdated
Show resolved
Hide resolved
src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs
Outdated
Show resolved
Hide resolved
src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs
Outdated
Show resolved
Hide resolved
src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs
Outdated
Show resolved
Hide resolved
src/Controls/src/Core/Platform/GestureManager/LongPressGestureHandler.Windows.cs
Outdated
Show resolved
Hide resolved
src/Controls/src/Core/Platform/GestureManager/LongPressGestureHandler.Windows.cs
Outdated
Show resolved
Hide resolved
src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs
Outdated
Show resolved
Hide resolved
|
@copilot After applying the last suggestion we face this build error: src\Controls\src\Core\Platform\GestureManager\GesturePlatformManager.iOS.cs(384,8): error CS0841: (NETCORE_ENGINEERING_TELEMETRY=Build) Cannot use local variable 'longPressRecognizer' before it is declared I prefer to make it work with the syntax proposed by @pictos can you see if you can fix it? If not, revert the changes in commit 40b5aa5 |
|
@jfversluis I've opened a new pull request, #33482, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@jfversluis the issue is that copilot is using the same variable name after the type check,
|
|
@pictos this copilot? :P
|
|
@jfversluis yeap, it should be smarter than me 😶🌫️ xD |
src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs
Outdated
Show resolved
Hide resolved
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@copilot Build error in the UI test sample app src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellFlyoutRenderer.cs(245,24): Error RS0016: Symbol '~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) Please don't touch the Public API declarations other than the ones you need for this change |
|
@jfversluis I've opened a new pull request, #33504, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/rebase |
- Created LongPressGestureHandler.cs using Tizen NUI's native LongPressGestureDetector - Uses native gesture state tracking (Started/Finished/Cancelled) - Configured with MinimumPressDuration and NumberOfTouchesRequired - Integrated into GestureDetector.CreateHandler() - Native support for full state lifecycle (Started → Completed/Canceled) - Sets e.Handled = false for gesture coexistence - Fires LongPressed and LongPressing events with proper state transitions - All 99 gesture tests passing (17 LongPress + 82 existing)
BREAKING CHANGE: Replaced custom timer-based Android implementation with native Android GestureDetector.OnLongPress() integration. Changes: - Removed custom timer-based LongPressGestureHandler.Android.cs from GestureManager/ - Created new native LongPressGestureHandler.cs in Platform/Android/ - Integrated into InnerGestureListener.OnLongPress() alongside DragAndDrop - Uses Android's native GestureDetector with system-default long press timeout - Updated InnerGestureListener.EnableLongPressGestures to include LongPress gestures - Removed manual OnTouchEvent handling from GesturePlatformManager Benefits: - Native Android gesture detection (better system integration) - No manual timer lifecycle management - Consistent with other gesture implementations (Tap, Pan, Swipe) - Proper integration with Android's gesture system Platform Limitation (Documented): - Android uses system-default long press timeout (~400ms from ViewConfiguration) - MinimumPressDuration property is IGNORED on Android (cannot be configured per gesture) - Added XML documentation remarks noting this limitation Technical Details: - TapAndPanGestureDetector.IsLongpressEnabled now returns true when LongPress gestures present - LongPressGestureHandler follows same pattern as TapGestureHandler/PanGestureHandler - OnLongPress fires LongPressed and LongPressing(Completed) events - All 99 gesture tests passing (17 LongPress + 82 existing)
Created comprehensive UI tests to verify LongPress works correctly with other gestures. Test Page (TestCases.HostApp/Issues/LongPressGestureInteraction.xaml): - Test 1: LongPress + Tap on same element - Test 2: LongPress + Swipe (swipe cancels long press) - Test 3: LongPress in ScrollView (fires when still, cancels on scroll) - Test 4: Multiple independent LongPress recognizers NUnit Tests (TestCases.Shared.Tests/Tests/Issues/LongPressGestureInteraction.cs): - LongPressWithTap_BothFireIndependently() * Quick tap fires Tap only * Long hold fires LongPress only - LongPressWithSwipe_SwipeCancelsLongPress() * Swipe movement cancels LongPress (movement threshold) - LongPressInScrollView_FiresWhenStill() * LongPress fires when holding still in ScrollView - MultipleLongPress_AllWorkIndependently() * Each LongPress recognizer works on its own element independently Test Design: - Uses AutomationIds for Appium element location - Counter labels for verification - TouchAndHold() and SwipeLeftToRight() Appium methods - Validates gesture isolation (Tap doesn't fire LongPress, etc.) - Validates movement cancellation (swipe cancels long press) Category: [Category(UITestCategories.Gestures)] Notes: - TestCases.HostApp has pre-existing build errors (unrelated to this PR) - TestCases.Shared.Tests builds successfully - Tests follow existing MAUI UI test patterns (inherit from _IssuesUITest) - Ready for CI execution when HostApp build issues are resolved
…tureRecognizer - Added LongPressGestureGalleryPage to Controls.Sample with 5 interactive demos: * Basic long press (500ms default) * Custom duration (1000ms) * Movement sensitive (5px threshold) * Combined with tap gesture * Real-time state and position display - Added to GesturesViewModel gallery list - Created comprehensive performance test suite (9 tests): * Memory leak detection (creation, add/remove, event subscription) * Performance benchmarks (creation, property changes, events, commands) * State change stress testing - All 9 performance tests passing (total: 109/109 tests) - Tests verify: * No memory leaks from recognizer creation or event subscriptions * Fast creation (10k recognizers < 1s) * Fast property changes (10k changes < 500ms) * Fast event firing (20k events < 500ms) * Fast command execution (10k commands < 500ms) * Functional after heavy state changes (4000 state transitions)
- Windows: Fix multiple LongPress recognizers on same view - now fires all recognizers instead of just the last one - Windows: Add comment documenting edge case limitation (shared timer duration) - Tizen: Remove incorrect comment - Tizen DOES support configurable MinimumPressDuration via SetMinimumHoldingTime - All 108 gesture tests still passing
- Use type aliases to disambiguate Windows.Foundation.Point vs Microsoft.Maui.Graphics.Point - WinPoint = Windows.Foundation.Point (for native pointer position) - MauiPoint = Microsoft.Maui.Graphics.Point (for MAUI gesture APIs) - Fixes build error CS0104 on Windows platform
Add missing 'using Microsoft.Maui;' to resolve CS0103 error. TextAlignment enum is in Microsoft.Maui namespace.
- LongPressedEventArgs: Use Position property (not GetPosition method), no State property - LongPressingEventArgs: Use Status property (not State), add Position display - LongPressed event always means completed, so hardcode state display
TouchAndHold() takes only element ID parameter, not TimeSpan. The method holds for 2 seconds by default (hardcoded in implementation), which is more than sufficient for our 500ms default threshold. Fixes CS1501 compilation errors on lines 31, 62, 77, 87.
Changes requested by @pictos: - iOS: Invert if statements to reduce nesting (lines 383, 391) - iOS: Cache ShouldRecognizeSimultaneously lambda to avoid allocation - Windows: Use null-coalescing assignment (??=) for handler initialization - Windows: Invert if statement in SubscribeEvents to reduce nesting - Windows: Move GestureRecognizers null check earlier in OnPointerMoved to avoid unnecessary calculations when recognizers collection is null These changes reduce cyclomatic complexity and improve performance.
Renamed 'uiRecognizer' to 'longPressUiRecognizer' to avoid CS0136 error. When the if block was inverted, the variable was moved into the same scope as other gesture recognizers that also use 'uiRecognizer', causing a name collision. Fixes: error CS0136: A local or parameter named 'uiRecognizer' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter
…anager.iOS.cs Co-authored-by: Pedro Jesus <pedrojesus.cefet@gmail.com>
BREAKING CHANGE: This aligns LongPressedEventArgs and LongPressingEventArgs with the established MAUI gesture pattern used by TapGestureRecognizer and PointerGestureRecognizer. The GetPosition(Element? relativeTo) pattern allows: - Getting position relative to any element in the visual tree - Getting position relative to the window (pass null) - Proper support for positioning context menus/tooltips Changes: - LongPressedEventArgs: Position property → GetPosition(Element?) method - LongPressingEventArgs: Position property → GetPosition(Element?) method - Updated public constructors to not require position - Internal constructors accept Func<IElement?, Point?> for lazy evaluation - All platform handlers (iOS, Android, Windows, Tizen) updated with position calculation logic - PublicAPI files updated with new signatures - Sample gallery and unit tests updated This is a preview-only change that must be made before GA to avoid breaking user code later.
- Use App.LongPress() instead of App.TouchAndHold() on MacCatalyst (TouchAndHold uses touch pointer type which isn't supported on Mac) - Fix swipe direction to match SwipeGestureRecognizer.Direction="Left" (SwipeRightToLeft swipes in the Left direction)
- Add TapElement() helper to UtilExtensions for coordinate-based tap - Use TapElement instead of App.Tap for TapGestureRecognizer elements - App.Tap uses element.Click() which triggers Android's performClick() - MAUI uses GestureDetector for tap recognition, not OnClickListener - TapElement uses TapCoordinates which sends actual touch events
Swipe and LongPress gestures on the same element conflict on these platforms: - Windows: Manipulation system captures pointer events, preventing LongPressGestureHandler's PointerMoved-based cancellation from working - macOS Catalyst: Discrete UISwipeGestureRecognizer conflicts with continuous UILongPressGestureRecognizer during Appium simulation
The XAML page used FindByName to update labels, but labels only had AutomationId set (not x:Name). FindByName searches by x:Name, so it always returned null and label text never updated from initial values. Converting to pure C# with direct label references fixes the root cause. This also follows UI test guidelines (prefer C# over XAML for non-XAML tests) and may resolve the Windows page rendering issue.
Elements at the bottom of the page were below the viewport, causing Appium to time out finding them on Android and failing to interact with them on iOS. Removed the outer ScrollView, reduced element heights and spacing so all 4 test areas fit on a single screen.
fb9f089 to
8ca54da
Compare
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
- Add HasAccessibleLongPressGesture() extension matching TapGesture pattern - iOS: Set UIAccessibilityTrait.Button for LongPress elements (VoiceOver) - Android: Enable keyboard focus and confirm key activation (TalkBack)
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Windows: - Fix timer leak: cancel existing timers on new press - Fix multi-recognizer: per-recognizer timer tracking via Dictionary - Fix double-fire: per-recognizer HashSet instead of shared bool - Fix collection safety: snapshot recognizers in OnPointerMoved iOS/Mac Catalyst: - Fix tap+longpress exclusivity: add RequireGestureRecognizerToFail so taps don't fire during long presses (matching Android behavior) - Add PropertyChanged handler for runtime property updates (MinimumPressDuration, AllowableMovement, NumberOfTouchesRequired) Android: - Fix dispose: null _longPressGestureHandler in Dispose - Fix accessibility: if/else if for Tap vs LongPress on Enter/Space - Update outdated comment in TapAndPanGestureDetector Core: - Add input validation: MinimumPressDuration >= 0, NumberOfTouchesRequired > 0, AllowableMovement >= 0 Tests: - Add 3 validation unit tests - Replace deprecated Frame with Border in UI test host page - Fix nullable annotations in test file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description
Implements LongPressGestureRecognizer for .NET MAUI 11, a comprehensive cross-platform gesture recognizer that fires when the user presses and holds on an element for a configurable duration.
Addresses: #8675
Implementation Overview
Core API
LongPressGestureRecognizer: Main recognizer class with 5 bindable properties, 2 events, and Command supportLongPressedEventArgs: Event args for gesture completionLongPressingEventArgs: Event args for real-time state updatesPlatform Implementations
UILongPressGestureRecognizerGestureDetector.OnLongPress()DispatcherTimerLongPressGestureDetectorKey Features
MinimumPressDurationproperty (default 500ms)AllowableMovementproperty cancels gesture if exceeded (default 10px)NumberOfTouchesRequired(iOS/MacCatalyst/Tizen only)Stateproperty withOneWayToSourcebindingCommandandCommandParameterGesture Coexistence Strategy
ShouldRecognizeSimultaneously = truee.Handled = falsepatterne.Handledset)e.Handled = falseTesting
Unit Tests
Performance Benchmarks
All benchmarks exceed targets by 50-250x:
UI Tests
Note: UI tests compile successfully but cannot be executed locally due to pre-existing TestCases.HostApp build errors in net11.0 branch (unrelated to this PR). They will be validated in CI.
Sample Gallery
LongPressGestureGalleryPageadded to Controls.SamplePlatform-Specific Notes
Android Limitation
The
MinimumPressDurationproperty is not configurable on Android due to platform limitations. Android uses the system-default long press timeout (typically ~400ms fromViewConfiguration.getLongPressTimeout()). This is documented in XML remarks on the property.Windows Edge Case
If multiple
LongPressGestureRecognizerinstances with different durations are added to the same view, they will all use the last recognizer's duration. This is an extremely rare edge case and is documented in code comments.API Changes
Added to all PublicAPI.Unshipped.txt files (7 platforms):
LongPressGestureRecognizerclass (5 properties, 2 events, 1 command)LongPressedEventArgsclassLongPressingEventArgsclassMigration Path
For users currently using
CommunityToolkit.Maui.TouchBehavior:Before (CommunityToolkit):
After (MAUI built-in):
Example Usage
Basic Long Press
With Custom Duration and State Tracking
Code-Behind
Breaking Changes
None - this is a new API in .NET 11.
Checklist