AppoMobi.Maui.Gestures 3.10.4

dotnet add package AppoMobi.Maui.Gestures --version 3.10.4
                    
NuGet\Install-Package AppoMobi.Maui.Gestures -Version 3.10.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AppoMobi.Maui.Gestures" Version="3.10.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AppoMobi.Maui.Gestures" Version="3.10.4" />
                    
Directory.Packages.props
<PackageReference Include="AppoMobi.Maui.Gestures" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AppoMobi.Maui.Gestures --version 3.10.4
                    
#r "nuget: AppoMobi.Maui.Gestures, 3.10.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AppoMobi.Maui.Gestures@3.10.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AppoMobi.Maui.Gestures&version=3.10.4
                    
Install as a Cake Addin
#tool nuget:?package=AppoMobi.Maui.Gestures&version=3.10.4
                    
Install as a Cake Tool

AppoMobi.Gestures

Cross-framework gesture recognition ecosystem for .NET. One interface (IGestureListener), shared data models, multiple platform implementations.

Used by DrawnUI.


Packages

Package Target Description
AppoMobi.Gestures netstandard2.0 Core contracts: IGestureListener, all event/data types. Zero dependencies.
AppoMobi.Maui.Gestures net9.0 / net10 multi-platform .NET MAUI implementation via RoutingEffect.
AppoMobi.Blazor.Gestures net9.0 / net10.0 blazor web Blazor implementation via JS pointer/wheel interop.
# MAUI
dotnet add package AppoMobi.Maui.Gestures

# Blazor
dotnet add package AppoMobi.Blazor.Gestures

v2 migration: shared types (IGestureListener, TouchActionEventArgs, enums…) moved from AppoMobi.Maui.Gestures namespace to AppoMobi.Gestures. Add using AppoMobi.Gestures; to files that reference them.


Samples

The repo includes 2 sample apps:

  • samples/MauiSample — .NET MAUI sample app for AppoMobi.Maui.Gestures
  • samples/BlazorWasmSample — Blazor WebAssembly sample app for AppoMobi.Blazor.Gestures

There is also dev/GesturesTester, which is a manual validation app used for gesture testing during development.


The Interface

Both MAUI and Blazor implementations route all gesture events through one interface. Implement it to receive processed gesture results regardless of platform:

public interface IGestureListener
{
    void OnGestureEvent(TouchActionType type, TouchActionEventArgs args, TouchActionResult action);
    bool InputTransparent { get; }
}

TouchActionResult is the processed high-level result:

Value Meaning
Down First contact
Up Contact ended
Tapped Quick tap within move threshold
LongPressing Held past long-press duration
Panning Moving with contact
Wheel Mouse wheel / trackpad scroll
Pointer Mouse/pen hover (no button held)

TouchActionEventArgs carries everything: pixel location, source coordinate scale, velocity, distance totals, multi-touch manipulation (scale/rotation), mouse button state, pointer device type.


.NET MAUI

Gestures are handled by a MAUI effect. Use attached properties for command-driven scenarios, or force-attach the effect when your control implements IGestureListener directly.

Setup

// MauiProgram.cs
builder.UseGestures();

Basic Usage in XAML

<ContentPage xmlns:touch="clr-namespace:AppoMobi.Gestures;assembly=AppoMobi.Maui.Gestures">

    <Label Text="Tap Me!"
           touch:TouchEffect.CommandTapped="{Binding TapCommand}" />

    <Frame touch:TouchEffect.CommandTapped="{Binding ItemTappedCommand}"
           touch:TouchEffect.CommandTappedParameter="{Binding .}"
           touch:TouchEffect.CommandLongPressing="{Binding LongPressCommand}">
        <Label Text="Long press or tap me!" />
    </Frame>

</ContentPage>

Basic Usage in Code-Behind

TouchEffect.SetCommandTapped(myView, TapCommand);
TouchEffect.SetCommandTappedParameter(myView, itemData);
TouchEffect.SetCommandLongPressing(myView, LongPressCommand);
TouchEffect.SetForceAttach(myView, true);
TouchEffect.SetShareTouch(myView, TouchHandlingStyle.Lock);

Enhanced Usage for Custom Controls

Attach the effect without command bindings when your control handles gestures itself:

<draw:Canvas
    touch:TouchEffect.ForceAttach="True" />

Or in code-behind:

TouchEffect.SetForceAttach(myView, true);

IGestureListener in MAUI

public class MyControl : ContentView, IGestureListener
{
    public MyControl()
    {
        TouchEffect.SetForceAttach(this, true);
    }

    public bool InputTransparent => false;

    public void OnGestureEvent(TouchActionType type, TouchActionEventArgs args, TouchActionResult action)
    {
        switch (action)
        {
            case TouchActionResult.Down:
                _startPoint = args.Location;
                break;

            case TouchActionResult.Panning:
                var delta = args.Distance.Delta;
                var velocity = args.Distance.Velocity;
                if (args.Manipulation != null)
                {
                    var scale = args.Manipulation.Scale;
                    var rotation = args.Manipulation.Rotation;
                }
                break;

            case TouchActionResult.Tapped:
                ExecuteTapAction();
                break;

            case TouchActionResult.LongPressing:
                ShowContextMenu();
                break;
        }
    }
}

Blazor

Setup

// Program.cs
builder.Services.AddBlazorGestures();

No JS imports needed — the package serves canvasGestures.js automatically from _content/AppoMobi.Blazor.Gestures/canvasGestures.js.

Usage

Inject TouchEffect, attach it to an ElementReference after render, pass your IGestureListener:

@inject TouchEffect Gestures
@implements IAsyncDisposable
@using AppoMobi.Gestures

<div @ref="_container" style="width:400px;height:300px;touch-action:none;">
    
</div>

@code {
    private ElementReference _container;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await Gestures.AttachAsync(_container, new MyGestureListener());
    }

    public async ValueTask DisposeAsync() => await Gestures.DisposeAsync();
}

Important: set touch-action: none on the element so the browser does not consume pointer events before JS sees them.

IGestureListener in Blazor

Same interface, same event args — only the host differs:

using AppoMobi.Gestures;

public class MyGestureListener : IGestureListener
{
    public bool InputTransparent => false;

    public void OnGestureEvent(TouchActionType type, TouchActionEventArgs args, TouchActionResult action)
    {
        switch (action)
        {
            case TouchActionResult.Tapped:
                Console.WriteLine($"Tapped at {args.Location}");
                break;

            case TouchActionResult.Panning:
                Console.WriteLine($"Panning delta {args.Distance.Delta}");
                break;

            case TouchActionResult.LongPressing:
                Console.WriteLine("Long press!");
                break;

            case TouchActionResult.Wheel:
                Console.WriteLine($"Wheel delta {args.Wheel?.Delta}");
                break;

            case TouchActionResult.Pointer:
                // Mouse/pen hover — no button held
                Console.WriteLine($"Hover at {args.Location}");
                break;
        }
    }
}

Blazor Configuration

// Per-instance tuning (before AttachAsync)
Gestures.LongPressTimeMs = 1000;
Gestures.Draggable = true;      // Don't cancel Moved when pointer leaves element bounds

// Static scaling default for Blazor events
TouchEffect.Density = 1f;       // CSS pixels = points on web; set to devicePixelRatio for physical-pixel canvases

Pointer and Wheel Data

Mouse, pen and touch events all populate args.Pointer:

if (args.Pointer != null)
{
    var device = args.Pointer.DeviceType;       // Mouse / Pen / Touch
    var button = args.Pointer.Button;           // Left / Right / Middle / XButton1…
    var pressed = args.Pointer.PressedButtons;  // Flags of all currently held buttons
    var pressure = args.Pointer.Pressure;       // 0.0–1.0 (pen), 1.0 (mouse)
}

if (args.Wheel != null)
{
    var delta = args.Wheel.Delta;   // Positive = wheel up / away from the user
    var center = args.Wheel.Center; // Location of wheel event
}

Touch Handling Modes

Mode Description
Default Normal behavior
Lock Blocks all parent input — use for canvases, drawing surfaces
Manual Dynamic control via WIllLock at runtime — use for carousels inside ScrollView
SoftLock Shares gestures with parent native scrolling surfaces until your control clearly takes over
Disabled Same as InputTransparent = true

Manual Mode Example

public class MyCarousel : ContentView, IGestureListener
{
    private bool _isHandling = false;

    public MyCarousel()
    {
        TouchEffect.SetShareTouch(this, TouchHandlingStyle.Manual);
        TouchEffect.SetForceAttach(this, true);
    }

    public bool InputTransparent => false;

    public void OnGestureEvent(TouchActionType type, TouchActionEventArgs args, TouchActionResult action)
    {
        var effect = TouchEffect.GetFrom(this);

        switch (action)
        {
            case TouchActionResult.Down:
                _isHandling = false;
                break;

            case TouchActionResult.Panning:
                var dx = Math.Abs(args.Distance.Delta.X);
                var dy = Math.Abs(args.Distance.Delta.Y);

                if (!_isHandling)
                {
                    if (dx > dy && dx > 5)
                    {
                        _isHandling = true;
                        effect.WIllLock = ShareLockState.Locked;    // block parent ScrollView
                    }
                    else if (dy > 5)
                    {
                        effect.WIllLock = ShareLockState.Unlocked;  // let parent scroll
                        return;
                    }
                }

                if (_isHandling)
                    ScrollBy(args.Distance.Delta.X);
                break;

            case TouchActionResult.Up:
                if (_isHandling)
                    SnapToNearestItem();
                break;
        }
    }
}

Coordinate Scaling

For cases when your app renders with a scale different from the native area gestures were attached to (Blazor etc), the following could help.

TouchActionEventArgs.Scale stores the source scale used when the event was produced. If your rendering surface uses a different scale, call args.Rescale(renderingScale) inside OnGestureEvent and use the returned event for hit testing or game logic.

var gestureArgs = args;

if (renderingScale != args.Scale)
{
    gestureArgs = args.Rescale(renderingScale);
}

UseGesture(gestureArgs);

Rescale uses the ratio renderingScale / args.Scale internally and returns the original instance when no rescaling is needed.

Gesture Data Reference

// TouchActionEventArgs
args.Scale             // float  — source scale of the coordinates carried by this event
args.Location          // PointF — current hit position in pixels (CSS pixels on web)
args.StartingLocation  // PointF — where the gesture began
args.NumberOfTouches   // int   — active touch/pointer count
args.IsInContact       // bool  — gesture started inside the view
args.IsInsideView      // bool  — current hit is inside view bounds
args.PreventDefault    // bool  — set true in LongPressing to suppress Tapped

// Distance info (all in pixels)
args.Distance.Delta        // PointF — movement since last event
args.Distance.Total        // PointF — total movement from start
args.Distance.Velocity     // PointF — pixels/second
args.Distance.TotalVelocity

// Multi-touch (null for single touch)
args.Manipulation?.Scale         // double — scale change this frame
args.Manipulation?.ScaleTotal    // double — scale from gesture start
args.Manipulation?.Rotation      // double — rotation change this frame (degrees)
args.Manipulation?.RotationTotal // double — rotation from start
args.Manipulation?.Center        // PointF — centroid of all active pointers

Static Configuration

// MAUI — global defaults
TouchEffect.LongPressTimeMsDefault = 1500;          // ms
TouchEffect.TappedCancelMoveThresholdPoints = 16f;  // points; movement above this cancels tap
TouchEffect.LogEnabled = true;

// Blazor — static defaults before creating instances
TouchEffect.LongPressTimeMsDefault = 1500;
TouchEffect.TappedCancelMoveThresholdPoints = 16f;

Building Your Own Platform Implementation

Reference AppoMobi.Gestures only, implement IGestureListener on your controls, and bridge your platform's pointer/touch events to TouchActionEventArgs. Use TouchActionEventArgs.FillDistanceInfo for velocity and distance tracking, and MultitouchTracker for pinch/rotation:

// On each raw platform pointer event:
var args = new TouchActionEventArgs(pointerId, TouchActionType.Moved, new PointF(x, y), context);
TouchActionEventArgs.FillDistanceInfo(args, previousArgs);
var manipulation = _tracker.AddMovement(pointerId, args.Location);
args.Manipulation = manipulation;
// ... route to your listener

What's New

  • 3.10.2 — Invert Blazor Wheel direction to match other platforms.
  • 3.10.1.NET 10 support added for both AppoMobi.Maui.Gestures and AppoMobi.Blazor.Gestures, while keeping .NET 9 targets in place. Adds TouchActionEventArgs.Scale for source-coordinate scaling and TouchActionEventArgs.Rescale(float) for remapping gesture data to the consumer rendering scale.
  • 1.11.9.2 — Android: built-in programmatic tap for sensitive screens (Galaxy S sends microscopic pans instead of tap). TappedCancelMoveThresholdPoints defaults to 16 (was 5).

Contributing

  1. 💬 Discussion
  2. 🐛 Issue
  3. 🔧 Pull Request
Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-android35.0 is compatible.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-ios18.0 is compatible.  net9.0-maccatalyst was computed.  net9.0-maccatalyst18.0 is compatible.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net9.0-windows10.0.19041 is compatible.  net10.0 is compatible.  net10.0-android was computed.  net10.0-android36.0 is compatible.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-ios26.0 is compatible.  net10.0-maccatalyst was computed.  net10.0-maccatalyst26.0 is compatible.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net10.0-windows10.0.19041 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on AppoMobi.Maui.Gestures:

Package Downloads
DrawnUi.Maui

Cross-platform rendering engine for .NET MAUI to draw your UI with SkiaSharp

DrawnUi.Maui.Core

Cross-platform rendering engine for .NET MAUI to draw your UI with SkiaSharp

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on AppoMobi.Maui.Gestures:

Repository Stars
taublast/DrawnUi
Rendering engine for .NET MAUI, Blazor, OpenTK and pure .NET powered by SkiaSharp 🎨
Version Downloads Last Updated
3.10.4 773 5/22/2026
3.10.2 440 5/19/2026
3.10.1 143 5/15/2026
1.11.9.2 2,564 3/4/2026
1.11.9.1 2,205 12/7/2025
1.11.1 1,820 10/18/2025
1.9.7 3,522 4/28/2025
1.9.5 319 4/14/2025
1.9.4 897 3/25/2025
1.9.3.1 1,982 12/31/2024
1.9.2.5 574 12/1/2024
1.9.2.2 221 12/1/2024
1.9.2.1 626 11/24/2024
1.9.1.1 481 11/17/2024
1.8.1.2 1,856 7/14/2024
1.2.2.1 981 5/29/2024
1.2.1.1 567 5/12/2024
1.0.8.4 644 4/3/2024
1.0.8.3 304 4/3/2024
1.0.8.2 539 3/4/2024
Loading failed