Skip to content

Add platform-agnostic gesture recognition system (SKGestureDetector + SKGestureTracker)#326

Open
Copilot wants to merge 108 commits intomainfrom
copilot/copy-skia-to-maui
Open

Add platform-agnostic gesture recognition system (SKGestureDetector + SKGestureTracker)#326
Copilot wants to merge 108 commits intomainfrom
copilot/copy-skia-to-maui

Conversation

Copy link
Contributor

Copilot AI commented Feb 5, 2026

Summary

Adds a complete, platform-agnostic gesture recognition system to SkiaSharp.Extended with 15 new source files, 11 test files (181 tests), comprehensive docs, and Blazor + MAUI sample apps.

Staging

Architecture

Two-layer design — both in core SkiaSharp.Extended (netstandard2.0 + net9.0), no platform dependencies:

  • SKGestureDetector — low-level state machine (None → Detecting → Panning/Pinching). Processes raw touch input and fires discrete gesture events. Uses TimeProvider for deterministic testing.
  • SKGestureTracker — high-level wrapper that manages a cumulative SKMatrix transform (pan/zoom/rotate), runs fling and double-tap-zoom animations, provides drag lifecycle, and feature toggles.
SkiaSharp.Extended (Core)
└── Gestures/
    ├── SKGestureDetector.cs           # Touch → gesture state machine (679 lines)
    ├── SKGestureDetectorOptions.cs    # Detection thresholds (slop, timing, velocity)
    ├── SKGestureTracker.cs            # Gesture → transform matrix + animations (931 lines)
    ├── SKGestureTrackerOptions.cs     # Scale limits, zoom, fling, scroll config
    ├── SKFlingTracker.cs              # Velocity estimation for fling
    ├── SK*GestureEventArgs.cs (8)     # Typed EventArgs for each gesture
    └── SKGestureLifecycleEventArgs.cs # GestureStarted/Ended lifecycle

Supported Gestures

Gesture Events Notes
Tap TapDetected Single finger, fires on release
Double Tap DoubleTapDetected Configurable slop + timing
Long Press LongPressDetected Timer-based, configurable duration
Pan PanDetected Single finger drag with location tracking
Pinch PinchDetected Two+ finger scale with focal point
Rotate RotateDetected Two+ finger rotation
Fling FlingDetected/Updated/Completed Momentum animation with configurable friction
Drag DragStarted/Updated/Ended App-level object dragging lifecycle
Scroll ScrollDetected Mouse wheel zoom
Hover HoverDetected Mouse movement without contact

Key Design Decisions

  • Coordinate-space-agnostic: The tracker operates on whatever numbers you pass in. MAUI passes device pixels directly; Blazor multiplies CSS pixels by devicePixelRatio.
  • (0,0) matrix origin: Transform uses Scale · Rotate · Translate(offset) — no view-center dependency. SetScale/SetRotation accept an optional pivot parameter.
  • Options pattern: All thresholds in SKGestureDetectorOptions (detection) and SKGestureTrackerOptions (tracking), with full validation.
  • Feature toggles: Each gesture type can be enabled/disabled at runtime via Is*Enabled properties.
  • Handled pattern: Handled property on tap/drag/pan EventArgs allows consumers to intercept gestures (e.g., DragStarted.Handled = true suppresses pan).
  • Sealed classes: Both SKGestureDetector and SKGestureTracker are sealed — no fragile base class issues, clean Dispose without Dispose(bool).

Test Coverage

181 tests across 11 files covering:

  • All gesture recognition paths and state transitions
  • Options validation (all ArgumentOutOfRangeException checks)
  • EventArgs property verification with exact expected values
  • Multi-touch transitions (1↔2↔3 fingers)
  • Feature toggle behavior
  • Edge cases: cancel during gestures, double-dispose safety, touch ID reuse, boundary conditions
  • Fling/zoom animation mechanics

Documentation

  • Quick start: docs/docs/gestures.md
  • Event reference: docs/docs/gesture-events.md
  • Configuration guide: docs/docs/gesture-configuration.md
  • Full XML documentation on all public types, properties, events, and methods

Samples

  • MAUI: samples/SkiaSharpDemo/Demos/Gestures/ — multi-sticker canvas with pan/zoom/rotate/fling, full settings panel with all options
  • Blazor: samples/SkiaSharpDemo.Blazor/Pages/Gestures.razor — complete gesture demo with pointer events, configurable settings panel

Files Changed

41 files changed, +8,286 lines

  • 15 new source files in source/SkiaSharp.Extended/Gestures/
  • 11 test files in tests/SkiaSharp.Extended.Tests/Gestures/
  • 3 documentation pages + TOC update
  • 5 sample files (Blazor) + 3 sample files (MAUI)
  • PR monitor skill (.github/skills/pr-monitor/)

Copilot AI and others added 2 commits February 5, 2026 02:47
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Copilot AI changed the title [WIP] Copy SkiaSharp Extended code to MAUI and fix issues Port SKDynamicSurfaceView and SKGestureSurfaceView controls to MAUI Feb 5, 2026
Copilot AI requested a review from mattleibow February 5, 2026 02:59
Copy link
Collaborator

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please remove the dynamic surface iew from this PR and focus it on the gesture view. We should use the existing surface view pattern in the repo. Look at the other controls.

Please make sure we check and fix all bugs.

Please make sure we have fully documented this gesture view.

Please improve the code.

Keep working and iterating until it is perfect. After you have done work, go back and review. Make sure to review all the files and understand what it is trying to do and if it is doing it right.

This gesture view needs to support the pan, rotate, scale/zoom as well as tap and long press. It should have modes to tap and select then drag as well ass long press to select and drag.

This will have to work as some drawing canvas with stickers/images. The user should be able to perform operations.

Make sure to add a sample page in the demo app show all the features.

Please implement this fully and make sure we get 90% test coverage.

Also, please rebase on main.

Maybe see if we can extract some gesture engine that can be tested outside of a view and we can use it as part of future non maui platforms.

Copilot AI and others added 2 commits February 19, 2026 12:21
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 19, 2026

@copilot please remove the dynamic surface iew from this PR and focus it on the gesture view. We should use the existing surface view pattern in the repo. Look at the other controls.

Please make sure...

Changes implemented in commits 2eccdb4 and b8e6328:

Architecture Changes:

  • ✅ Removed SKDynamicSurfaceView - focusing solely on gesture view
  • ✅ Created testable SKGestureEngine in core library (SkiaSharp.Extended/Gestures/)
  • SKGestureSurfaceView now extends SKSurfaceView (following existing patterns)

Gesture Support:

  • Tap, double-tap, multi-tap
  • Long press
  • Pan/drag
  • Pinch to zoom
  • Rotation
  • Fling (with velocity)
  • Hover

Selection Modes:

  • Immediate - Start dragging immediately
  • TapToSelect - Tap to select, then drag
  • LongPressToSelect - Long press to select and start drag

Tests: 39 unit tests (all passing) covering gesture engine

Demo: Added GesturePage to sample app under "UI & CONTROLS"

The gesture engine is platform-agnostic and testable, enabling reuse for future non-MAUI platforms. Rebase on main is pending (cannot perform rebases in this environment).

Copy link
Collaborator

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this is looking great. Please look at the comments

private readonly SKGestureEngine _engine;
private SKCanvasView? _canvasView;
private IDispatcher? _dispatcher;
private IDispatcherTimer? _longPressTimer;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot does this need to be here? Can't the engine use a system bcl timer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da09be3. The IDispatcherTimer has been removed from the view. The engine now uses System.Threading.Timer internally for long press detection, making it fully self-contained and platform-agnostic.


// Get canvas view and subscribe to touch
var templateChild = GetTemplateChild("PART_DrawingSurface");
if (templateChild is SKCanvasView view)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to add SKGLView

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da09be3. OnApplyTemplate now supports both SKCanvasView and SKGLView:

var templateChild = GetTemplateChild("PART_DrawingSurface");
if (templateChild is SKCanvasView canvasView)
{
    _canvasView = canvasView;
    _canvasView.EnableTouchEvents = true;
    _canvasView.Touch += OnTouch;
}
else if (templateChild is SKGLView glView)
{
    _glView = glView;
    _glView.EnableTouchEvents = true;
    _glView.Touch += OnTouch;
}

/// <summary>
/// Represents the state of a touch point.
/// </summary>
internal readonly struct TouchState
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot these simple things can be record structs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept them as regular readonly structs since record structs require IsExternalInit which isn't available in netstandard2.0. The structs are now cleaner with properties instead of fields:

internal readonly struct TouchState
{
    public long Id { get; }
    public SKPoint Location { get; }
    public long Ticks { get; }
    public bool InContact { get; }
    // ...
}

private int _tapCount;
private GestureState _gestureState = GestureState.None;
private PinchState _pinchState;
private long? _selectedItemId;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot do we need to track the selected item in the engine? This feels like something every app may want to do differently. For example, they may want to select multiple or an object or an index or an id. So we need to support this, but the sample does selection. How can we do this better?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da09be3. Selection tracking has been completely removed from the engine (SelectedItemId, SelectionMode, SelectionChanged all removed).

Apps now handle selection themselves in event handlers. The sample demo shows this pattern - it maintains its own _selectedSticker in the page and updates it in OnTap/OnLongPress handlers. This allows apps to implement any selection model they need (single, multiple, by object, by ID, etc.).

…y structs, remove selection tracking

Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Copilot AI changed the title Port SKDynamicSurfaceView and SKGestureSurfaceView controls to MAUI Add SKGestureSurfaceView control to MAUI Feb 20, 2026
Copilot AI requested a review from mattleibow February 20, 2026 01:56
Copy link
Collaborator

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please help me understand


private readonly Dictionary<long, TouchState> _touches = new();
private readonly FlingTracker _flingTracker = new();
private readonly object _syncLock = new();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot what is this for? Is there a way to not need this? Can we say this is only usable on the UI thread?

What was the reason for this being added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce0911e. The _syncLock was added because System.Threading.Timer fires on a thread pool thread, creating a race with UI thread access to _touches.

I've removed it by using SynchronizationContext instead:

  • Engine captures SynchronizationContext.Current on first touch (from UI thread)
  • Timer callback posts to the captured context via _syncContext.Post()
  • All state access now happens on the UI thread - no locks needed

The engine is now explicitly documented as UI-thread-only, which is the natural usage pattern for touch handling.

Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Copilot AI requested a review from mattleibow February 20, 2026 06:22
mattleibow and others added 5 commits March 3, 2026 03:30
- Split gestures.md (359 lines) into quick-start overview + 2 detail pages
- Create gesture-events.md with detailed event reference
- Create gesture-configuration.md with options and customization
- Fix stale SkiaSharp.Extended.Gestures namespace references
- Move Gestures entry to SkiaSharp.Extended section in TOC
- Extract inline CSS from Gestures.razor into Gestures.razor.css

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ace refactor

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SKPanGestureEventArgs: Delta is now a calculated property (Location - PrevLocation); renamed PreviousLocation → PrevLocation
- SKDragGestureEventArgs: renamed to Location/PrevLocation, removed StartLocation, Delta is calculated
- SKScrollGestureEventArgs: DeltaX/DeltaY replaced with SKPoint Delta
- SKFlingGestureEventArgs: VelocityX/Y replaced with SKPoint Velocity, DeltaX/Y with SKPoint Delta
- All event args now follow consistent naming: X/PrevX pattern with calculated Delta

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…iles

- SKGestureDetectorTapTests.cs: tap, double-tap, long-press tests
- SKGestureDetectorPanTests.cs: pan detection tests
- SKGestureDetectorPinchRotationTests.cs: pinch, rotation, three-finger tests
- SKGestureDetectorFlingTests.cs: fling detection and animation tests
- SKGestureDetectorHoverScrollTests.cs: hover and mouse wheel tests
- SKGestureTrackerDragTests.cs: drag lifecycle tests
- SKGestureTrackerFlingTests.cs: fling animation tests
- SKGestureTrackerZoomScrollTests.cs: double-tap zoom and scroll zoom tests
- SKGestureTrackerTransformTests.cs: matrix, pivot, SetScale/SetRotation tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use full 'Previous' prefix instead of shorthand 'Prev' in
SKPanGestureEventArgs and SKDragGestureEventArgs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Collaborator

mattleibow commented Mar 3, 2026

Please do a review of the code diff and update the PR description and title. Make sure to look for the staging comment and then also add it as links in the description.

Also remove the regions from the tests as 1 region per test is not useful.

@mattleibow
Copy link
Collaborator

mattleibow commented Mar 3, 2026

Done — all three items addressed:

1. PR description & title updated

  • Title: "Add platform-agnostic gesture recognition system (SKGestureDetector + SKGestureTracker)"
  • Description rewritten with full architecture, gesture table, design decisions, test coverage, and file summary

2. Staging links added to description

3. Removed all regions from test files (commit 11c27885)

  • Removed 118 #region/#endregion lines across 11 test files
  • 204 gesture tests still passing

Review findings (from fresh diff review):

  • 🔴 _zoomPrevCumulative field is dead code (assigned but never read)
  • 🟡 Pinch/Rotate events fire even when disabled (only transform is suppressed)
  • 🟡 GetActiveTouchPoints uses LINQ allocations in 60Hz hot path
  • 🟡 DateTime.Now default TimeProvider is non-monotonic
  • 🟢 Doc refers to ZoomTo targetScale param but it's actually a multiplier

Let me know which of these to address.

Single-region-per-section is not useful when files are already
split by area. The test file names provide sufficient organization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Collaborator

mattleibow commented Mar 3, 2026

Please fix them all. Also note, the lottie icon in the blazor sidebar is not showing.

@mattleibow
Copy link
Collaborator

mattleibow commented Mar 3, 2026

All fixed in 0f0298c0:

Review findings fixed:

  1. 🔴 Dead _zoomPrevCumulative field — removed (was assigned but never read)
  2. 🟡 Event gatingPinchDetected, RotateDetected, FlingDetected now suppressed when their feature toggles are disabled (previously only the transform was suppressed)
  3. 🟡 LINQ allocations in hot path — replaced GetActiveTouchPoints() LINQ chain with zero-allocation loop + insertion sort (called at 60Hz on every touch move)
  4. 🟡 Non-monotonic TimeProvider — switched default from DateTime.Now.Ticks to Environment.TickCount64 * TicksPerMillisecond (immune to DST/NTP adjustments)
  5. 🟢 Doc param names — fixed ZoomTo docs: factor/focalPoint (not targetScale/pivot)

Blazor sidebar fix:
6. 🆕 Gestures icon — added missing .bi-hand-index-nav-menu CSS class with hand icon SVG

Test fix:

  • Fixed flaky Fling_EventuallyCompletes test — was mixing real and fake time providers, causing velocity calculation to be wrong under parallel test load

204 gesture tests passing.

…, icon

- Remove dead _zoomPrevCumulative field (assigned but never read)
- Gate PinchDetected/RotateDetected/FlingDetected events by feature toggles
- Replace LINQ in GetActiveTouchPoints with zero-allocation loop (60Hz hot path)
- Use monotonic Environment.TickCount64 instead of DateTime.Now for TimeProvider
- Fix ZoomTo param names in docs (factor/focalPoint, not targetScale/pivot)
- Add missing .bi-hand-index-nav-menu CSS for Gestures icon in Blazor sidebar
- Fix flaky Fling_EventuallyCompletes test (was mixing real/fake time providers)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Collaborator

Please do a thorough 3 agent review to find any issues. Make sure the API is consistent and follows .NET guidelines. Make sure the docs are up to date.

@mattleibow
Copy link
Collaborator

mattleibow commented Mar 3, 2026

Running a thorough 3-agent review now — checking API consistency against .NET guidelines, docs accuracy, and code quality. Will report back with findings.

…null guards, dispose safety

- Clamp() now handles NaN by returning min (prevents transform corruption)
- Scroll zoom scaleDelta clamped to 0.01 minimum (prevents zero/negative scale from fast trackpad swipes)
- TimeProvider setter throws ArgumentNullException on null (both detector and tracker)
- ZoomTo throws ObjectDisposedException after Dispose (prevents timer creation after dispose)
- Long press timer explicitly stopped when entering Pinching state
- Fixed XML doc: canvas.Concat pattern, InvalidateSurface, singular 'property'
- Added 4 new tests for null guard, dispose, and scroll zoom edge cases (208 total)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Collaborator

3-Agent Review Complete — All Findings Fixed

Review Summary

Three parallel review agents analyzed the full gesture system:

Agent Focus 🔴 Critical 🟡 Moderate 🟢 Minor
API/.NET Guidelines Public API surface, naming, XML docs 2 4 0
Documentation Accuracy Markdown docs vs code consistency 0 0 3
Code Quality & Bugs Logic, thread safety, edge cases 2 4 5

Critical Fixes (commit dbe5229d)

  1. NaN propagation through ClampClamp(NaN, min, max) returned NaN (all NaN comparisons are false), cascading through scale → offset → Matrix → blank canvas. Fixed: NaN now maps to min.

  2. Scroll zoom negative scaleDelta1f + delta * 0.1f with delta ≤ -10 (common on macOS trackpad momentum) produced scaleDelta ≤ 0, causing massive offset teleport. Fixed: clamped to Math.Max(0.01f, ...).

  3. TimeProvider null guard — Both SKGestureDetector.TimeProvider and SKGestureTracker.TimeProvider setters now throw ArgumentNullException on null.

  4. ZoomTo after Dispose — Could create a Timer after disposal. Now throws ObjectDisposedException.

  5. Long press timer leak — Timer was not stopped when entering Pinching state (2nd finger down). Now explicitly cancelled.

Minor XML Doc Fixes

  • Canvas example: SetMatrixSave/Concat/Restore pattern
  • InvalidateVisual()InvalidateSurface()
  • "properties contain" → "property contains" (singular)

Tests

208 gesture tests passing (was 204, +4 new tests for null guard, dispose, and scroll zoom edge cases).

Deferred Items (not bugs, acceptable for v1)

  • Thread pool timer callbacks (long press, fling, zoom) run without synchronization when SynchronizationContext is null — only affects non-UI scenarios (tests, console apps)
  • Duplicate ProcessTouchDown with same ID silently overwrites — matches platform behavior
  • isMouse parameter in ProcessTouchUp is dead — tracked for future API cleanup
  • Zoom animation overwrites concurrent scroll-wheel changes during 250ms window — acceptable UX tradeoff

…maui

# Conflicts:
#	docs/docs/toc.yml
#	samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor
#	samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css
@mattleibow
Copy link
Collaborator

please merge in the latest from main

@mattleibow
Copy link
Collaborator

mattleibow commented Mar 4, 2026

✅ Already done — origin/main was merged into this branch in commit 59c8828. Three conflicts were resolved (toc.yml, NavMenu.razor, NavMenu.razor.css) — kept both Pixel Comparer from main and Gestures from this branch. Already pushed.

@mattleibow
Copy link
Collaborator

testing testing

@mattleibow
Copy link
Collaborator

mattleibow commented Mar 4, 2026

Looking into this...\n\nI examined the comment (ID: 3994455699) with body:\n\n"testing testing"\n\nClassification: question — appears to be a test comment; no action required. If you intended something else, please clarify.

@mattleibow
Copy link
Collaborator

🟢 Monitoring active — received your test comment.

mattleibow and others added 2 commits March 4, 2026 13:40
Build fixes:
- Replace Environment.TickCount64 with DateTime.UtcNow.Ticks (unavailable on netstandard2.0)
- Replace ObjectDisposedException.ThrowIf with manual check (unavailable on netstandard2.0)

Test fixes:
- Fix xUnit2002 warnings: Assert.NotNull on SKPoint value type → Assert.NotEqual(default)
- Fix CS0219 warnings: remove unused tapRaised variables

New tests (9 added, 337 total):
- NaN and Infinity coordinate handling
- Use-after-dispose on detector (ProcessTouchDown/Move/Up)
- Zero-distance touch (no pan triggered)
- Reset clears all transform state
- SetScale clamps to MinScale/MaxScale
- Pinch and pan simultaneously both apply

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Collaborator

Production Readiness Review — 722366343

Build Fix ✅

Fixed 2 netstandard2.0 build errors that were breaking CI:

  • Environment.TickCount64DateTime.UtcNow.Ticks (unavailable on netstandard2.0)
  • ObjectDisposedException.ThrowIf → manual check (unavailable on netstandard2.0)

3-Agent API Review ✅

Category Status
.NET Naming Conventions ✅ Excellent
Event Names (consistent pattern) ✅ Consistent
EventArgs Naming (SK*GestureEventArgs) ✅ Perfect
XML Documentation ✅ Comprehensive
Property Validation ✅ Excellent
netstandard2.0 Compatibility ✅ Fixed
IDisposable Implementation ✅ Correct

One minor recommendation: Consider converting int ms properties (LongPressDuration, ZoomAnimationDuration, etc.) to TimeSpan for better .NET idiomaticity. This would be a breaking change.

Test Coverage ✅

337 tests passing (9 new edge case tests added):

  • NaN/Infinity coordinate handling
  • Use-after-dispose on detector
  • Zero-distance touch (no false pan)
  • Reset clears transform state
  • SetScale clamps to MinScale/MaxScale
  • Simultaneous pinch + pan

Test warnings fixed: xUnit2002 (Assert.NotNull on value type), CS0219 (unused variables).

Documentation ✅

  • 3 gesture doc pages (gestures.md, gesture-events.md, gesture-configuration.md)
  • No stale references to SKGestureEngine
  • toc.yml correctly structured
  • Blazor sample has gestures page with nav link
  • All public APIs have XML docs

Remaining Low-Priority Items

  • MAUI sample app (docs reference it but only Blazor demo exists)
  • intTimeSpan for timing properties (breaking change, defer to next version)

mattleibow and others added 4 commits March 5, 2026 22:58
…cale ordering; convert duration properties to TimeSpan

- Fix PinchDetected firing when IsPinchEnabled=false but IsPanEnabled=true
- Reset _tapCount/_lastTapTicks when entering Pinching state to prevent false double-tap
- Add SetScaleRange() method for atomic min/max scale configuration
- Convert LongPressDuration, ZoomAnimationDuration, FlingFrameInterval, ZoomAnimationInterval from int (ms) to TimeSpan
- Update all usages in implementation, tests, and MAUI sample
- Add tests for all bug fixes and SetScaleRange edge cases (411 total, all passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When IsPanEnabled is false, the pinch focal point is now fixed to where
the gesture started, preventing effective panning through zoom+move.
The _pinchFocalPointOverride field is cleared when the gesture ends.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace inline _pinchFocalPointOverride with shared GetEffectiveGesturePivot
helper method. Apply pivot locking to both pinch AND rotation handlers,
fixing rotation drift when pan is disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants