AgentSkillsCN

maui-gestures

.NET MAUI 应用中实现轻触、滑动、平移、捏合、拖拽以及指针手势识别器的指南。涵盖 XAML 与 C# 的使用方式,手势的组合应用,以及不同平台之间的差异。

SKILL.md
--- frontmatter
name: maui-gestures
description: >
  Guidance for implementing tap, swipe, pan, pinch, drag-and-drop, and pointer
  gesture recognizers in .NET MAUI applications. Covers XAML and C# usage,
  combining gestures, and platform differences.

.NET MAUI Gesture Recognizers

All gesture recognizers inherit from GestureRecognizer and are added via View.GestureRecognizers.

xml
<Image>
  <Image.GestureRecognizers>
    <TapGestureRecognizer Tapped="OnTapped" />
  </Image.GestureRecognizers>
</Image>
csharp
var tap = new TapGestureRecognizer();
tap.Tapped += OnTapped;
image.GestureRecognizers.Add(tap);

Summary

RecognizerKey PropertiesKey Events / CommandsNotes
TapGestureRecognizerNumberOfTapsRequired, ButtonsTapped, CommandDefault 1 tap, primary button
SwipeGestureRecognizerDirection, ThresholdSwiped, CommandThreshold default 100 DIU
PanGestureRecognizerTouchPointsPanUpdatedStatusType: Started/Running/Completed
PinchGestureRecognizerPinchUpdatedScale, ScaleOrigin, Status
DragGestureRecognizerCanDragDragStarting, DropCompletedAuto data for Text/Image controls
DropGestureRecognizerAllowDropDragOver, DropPlatform-specific PlatformArgs
PointerGestureRecognizerPointerEntered/Exited/Moved/Pressed/ReleasedMatching commands; enables PointerOver visual state

TapGestureRecognizer

PropertyTypeDefaultDescription
NumberOfTapsRequiredint1Taps needed to fire
ButtonsButtonsMaskPrimaryPrimary, Secondary, or both
CommandICommandFires on tap
CommandParameterobjectPassed to Command
xml
<Label Text="Tap me">
  <Label.GestureRecognizers>
    <TapGestureRecognizer NumberOfTapsRequired="2" Buttons="Primary"
                          Command="{Binding DoubleTapCommand}" />
  </Label.GestureRecognizers>
</Label>
csharp
var tap = new TapGestureRecognizer { NumberOfTapsRequired = 2, Buttons = ButtonsMask.Primary };
tap.Command = new Command(() => Debug.WriteLine("Double-tapped"));
label.GestureRecognizers.Add(tap);

In .NET 10 ClickGestureRecognizer is deprecated. Use TapGestureRecognizer (touch/stylus) and PointerGestureRecognizer (mouse hover/press) instead.


SwipeGestureRecognizer

PropertyTypeDefaultDescription
DirectionSwipeDirectionLeft, Right, Up, Down
Thresholduint100Minimum distance in DIU
CommandICommandFires on swipe

SwipedEventArgs: Direction, Parameter.

xml
<BoxView Color="Teal">
  <BoxView.GestureRecognizers>
    <SwipeGestureRecognizer Direction="Left" Threshold="150" Swiped="OnSwiped" />
    <SwipeGestureRecognizer Direction="Right" Swiped="OnSwiped" />
  </BoxView.GestureRecognizers>
</BoxView>
csharp
var swipe = new SwipeGestureRecognizer { Direction = SwipeDirection.Left, Threshold = 150 };
swipe.Swiped += (s, e) => Debug.WriteLine($"Swiped {e.Direction}");
boxView.GestureRecognizers.Add(swipe);

Add one recognizer per direction; a single recognizer handles only one.


PanGestureRecognizer

PropertyTypeDefaultDescription
TouchPointsint1Number of fingers required

PanUpdatedEventArgs: StatusType (Started, Running, Completed), TotalX, TotalY, GestureId.

xml
<Image Source="photo.png">
  <Image.GestureRecognizers>
    <PanGestureRecognizer PanUpdated="OnPanUpdated" />
  </Image.GestureRecognizers>
</Image>
csharp
var pan = new PanGestureRecognizer();
pan.PanUpdated += (s, e) => {
    if (e.StatusType == GestureStatus.Running)
    { image.TranslationX = e.TotalX; image.TranslationY = e.TotalY; }
};
image.GestureRecognizers.Add(pan);

On iOS TotalX/TotalY are relative to the start; on Android they are relative to the previous event. Normalize in your handler if needed.


PinchGestureRecognizer

PinchGestureUpdatedEventArgs: Scale, ScaleOrigin (Point), Status (Started, Running, Completed).

xml
<Image Source="photo.png">
  <Image.GestureRecognizers>
    <PinchGestureRecognizer PinchUpdated="OnPinchUpdated" />
  </Image.GestureRecognizers>
</Image>
csharp
var pinch = new PinchGestureRecognizer();
pinch.PinchUpdated += (s, e) =>
{
    if (e.Status == GestureStatus.Running)
        image.Scale = Math.Clamp(image.Scale + (e.Scale - 1), 0.5, 3);
};
image.GestureRecognizers.Add(pinch);

DragGestureRecognizer

PropertyTypeDefaultDescription
CanDragbooltrueEnables/disables dragging
EventArgs TypeKey Properties
DragStartingDragStartingEventArgsData (DataPackage), Cancel
DropCompletedDropCompletedEventArgsDragDropResult

Label and Image auto-populate data packages. For custom data, set DragStartingEventArgs.Data.

xml
<Label Text="Drag me" BackgroundColor="LightBlue">
  <Label.GestureRecognizers>
    <DragGestureRecognizer CanDrag="True" DragStarting="OnDragStarting" />
  </Label.GestureRecognizers>
</Label>
csharp
var drag = new DragGestureRecognizer { CanDrag = true };
drag.DragStarting += (s, e) =>
{
    e.Data.Text = viewModel.ItemId;
    e.Data.Properties["payload"] = viewModel.SelectedItem;
};
view.GestureRecognizers.Add(drag);

DropGestureRecognizer

PropertyTypeDefaultDescription
AllowDropboolfalseMust be true to receive drops
EventArgs TypeKey Properties
DragOverDragEventArgsAcceptedOperation, PlatformArgs
DropDropEventArgsData (DataPackageView), PlatformArgs
xml
<StackLayout BackgroundColor="LightGray">
  <StackLayout.GestureRecognizers>
    <DropGestureRecognizer AllowDrop="True" DragOver="OnDragOver" Drop="OnDrop" />
  </StackLayout.GestureRecognizers>
</StackLayout>
csharp
var drop = new DropGestureRecognizer { AllowDrop = true };
drop.Drop += async (s, e) => { var text = await e.Data.GetTextAsync(); };
target.GestureRecognizers.Add(drop);

PlatformArgs per platform:

PlatformDragOverDrop
AndroidPlatformArgs.DragEventPlatformArgs.DragEvent
iOS / Mac CatalystUIDropInteraction argsUIDropInteraction args
WindowsWinUI DragEventArgsWinUI DragEventArgs

PointerGestureRecognizer

EventCommand PropertyFires When
PointerEnteredPointerEnteredCommandPointer enters view bounds
PointerExitedPointerExitedCommandPointer leaves view bounds
PointerMovedPointerMovedCommandPointer moves within view
PointerPressedPointerPressedCommandButton pressed in view
PointerReleasedPointerReleasedCommandButton released in view

PointerEventArgs.GetPosition(relativeTo) returns a Point?. Adding this recognizer enables the PointerOver VisualState.

xml
<Border StrokeShape="RoundRectangle 8">
  <Border.GestureRecognizers>
    <PointerGestureRecognizer PointerEnteredCommand="{Binding HoverInCommand}"
                              PointerExitedCommand="{Binding HoverOutCommand}"
                              PointerMoved="OnPointerMoved" />
  </Border.GestureRecognizers>
  <VisualStateManager.VisualStateGroups>
    <VisualStateGroup Name="CommonStates">
      <VisualState Name="PointerOver">
        <VisualState.Setters>
          <Setter Property="BackgroundColor" Value="LightCyan" />
        </VisualState.Setters>
      </VisualState>
    </VisualStateGroup>
  </VisualStateManager.VisualStateGroups>
</Border>
csharp
var pointer = new PointerGestureRecognizer();
pointer.PointerMoved += (s, e) => Debug.WriteLine($"Pointer at {e.GetPosition(null)}");
border.GestureRecognizers.Add(pointer);

Best for mouse/stylus. On touch devices, pointer events fire but hover is not tracked.


Combining Multiple Gestures

Add multiple recognizers to the same collection. The platform resolves conflicts.

xml
<Image Source="card.png">
  <Image.GestureRecognizers>
    <TapGestureRecognizer Tapped="OnTapped" />
    <PanGestureRecognizer PanUpdated="OnPan" />
    <PinchGestureRecognizer PinchUpdated="OnPinch" />
    <PointerGestureRecognizer PointerEntered="OnHover" />
  </Image.GestureRecognizers>
</Image>

Combining pan + swipe on the same view can conflict on Android. Test or use one at a time.


Platform Differences

AreaiOS / Mac CatalystAndroidWindows
Pan TotalX/TotalYRelative to startRelative to previous eventRelative to start
Pointer hoverMouse/trackpad onlyMouse onlyMouse/pen
Cross-app drag-dropSupported (iPadOS/Mac)Not supportedSupported
Secondary button tapSupportedLong-press fallbackSupported

Quick Rules

  1. One SwipeGestureRecognizer per direction.
  2. Use PointerGestureRecognizer for hover effects, not TapGestureRecognizer.
  3. Set AllowDrop="True" on drop targets — it defaults to false.
  4. Normalize pan deltas across platforms if sub-pixel accuracy matters.
  5. Prefer commands over events for MVVM; both work identically.
  6. In .NET 10 use TapGestureRecognizer instead of deprecated ClickGestureRecognizer.