![]() |
VOOZH | about |
dotnet add package AppoMobi.Maui.Gestures --version 3.10.6
NuGet\Install-Package AppoMobi.Maui.Gestures -Version 3.10.6
<PackageReference Include="AppoMobi.Maui.Gestures" Version="3.10.6" />
<PackageVersion Include="AppoMobi.Maui.Gestures" Version="3.10.6" />Directory.Packages.props
<PackageReference Include="AppoMobi.Maui.Gestures" />Project file
paket add AppoMobi.Maui.Gestures --version 3.10.6
#r "nuget: AppoMobi.Maui.Gestures, 3.10.6"
#:package AppoMobi.Maui.Gestures@3.10.6
#addin nuget:?package=AppoMobi.Maui.Gestures&version=3.10.6Install as a Cake Addin
#tool nuget:?package=AppoMobi.Maui.Gestures&version=3.10.6Install as a Cake Tool
Cross-framework gesture recognition ecosystem for .NET. One interface (IGestureListener), shared data models, multiple platform implementations.
Used by DrawnUI.
| 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 fromAppoMobi.Maui.Gesturesnamespace toAppoMobi.Gestures. Addusing AppoMobi.Gestures;to files that reference them.
The repo includes 2 sample apps:
samples/MauiSample β .NET MAUI sample app for AppoMobi.Maui.Gesturessamples/BlazorWasmSample β Blazor WebAssembly sample app for AppoMobi.Blazor.GesturesThere is also dev/GesturesTester, which is a manual validation app used for gesture testing during development.
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.
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.
// MauiProgram.cs
builder.UseGestures();
<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>
TouchEffect.SetCommandTapped(myView, TapCommand);
TouchEffect.SetCommandTappedParameter(myView, itemData);
TouchEffect.SetCommandLongPressing(myView, LongPressCommand);
TouchEffect.SetForceAttach(myView, true);
TouchEffect.SetShareTouch(myView, TouchHandlingStyle.Lock);
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);
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;
}
}
}
// Program.cs
builder.Services.AddBlazorGestures();
No JS imports needed β the package serves canvasGestures.js automatically from _content/AppoMobi.Blazor.Gestures/canvasGestures.js.
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: noneon the element so the browser does not consume pointer events before JS sees them.
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;
}
}
}
// 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
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
}
| 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 |
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;
}
}
}
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.
// 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
// 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;
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
js file to avoid caching issues.Lock mode on Blazor..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.TappedCancelMoveThresholdPoints defaults to 16 (was 5).| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-android35.0 net9.0-android35.0 is compatible. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-ios18.0 net9.0-ios18.0 is compatible. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-maccatalyst18.0 net9.0-maccatalyst18.0 is compatible. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net9.0-windows10.0.19041 net9.0-windows10.0.19041 is compatible. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-android36.0 net10.0-android36.0 is compatible. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-ios26.0 net10.0-ios26.0 is compatible. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-maccatalyst26.0 net10.0-maccatalyst26.0 is compatible. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. net10.0-windows10.0.19041 net10.0-windows10.0.19041 is compatible. |
Showing the top 3 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 |
|
|
lp90.dui.preview
Cross-platform rendering engine for .NET MAUI to draw your UI with SkiaSharp |
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.6 | 145 | 6/12/2026 |
| 3.10.4 | 939 | 5/22/2026 |
| 3.10.2 | 451 | 5/19/2026 |
| 3.10.1 | 152 | 5/15/2026 |
| 1.11.9.2 | 2,697 | 3/4/2026 |
| 1.11.9.1 | 2,248 | 12/7/2025 |
| 1.11.1 | 1,907 | 10/18/2025 |
| 1.9.7 | 3,551 | 4/28/2025 |
| 1.9.5 | 323 | 4/14/2025 |
| 1.9.4 | 903 | 3/25/2025 |
| 1.9.3.1 | 1,995 | 12/31/2024 |
| 1.9.2.5 | 580 | 12/1/2024 |
| 1.9.2.2 | 225 | 12/1/2024 |
| 1.9.2.1 | 632 | 11/24/2024 |
| 1.9.1.1 | 485 | 11/17/2024 |
| 1.8.1.2 | 1,859 | 7/14/2024 |
| 1.2.2.1 | 984 | 5/29/2024 |
| 1.2.1.1 | 575 | 5/12/2024 |
| 1.0.8.4 | 648 | 4/3/2024 |
| 1.0.8.3 | 309 | 4/3/2024 |