Tutorial 49: Touch Input and Gestures

CNA — C++ XNA 4.0 reimplementation

ℹ

What you’ll learn

  • Polling TouchPanel.GetState() and reading a TouchLocation.
  • The TouchLocationState lifecycle of a single touch.
  • Enabling gesture recognition and reading a GestureSample.
  • Drag and pinch-to-zoom as worked examples.

Before you start — Tutorial 11: Handling Mouse Input — touch follows the same per-frame polling shape as the mouse. Touch input only produces real data on a touch-capable target: Android, or a touch-enabled desktop or web build. No touch screen? See “Testing without a touch screen” below.

⚠

Platform notes. This example assumes the default SDL3 platform implementation (CNA_PLATFORM=SDL3); platform selection is independent of the graphics renderer. According to the platform capability tables, touch events are reported by the SDL3 implementation (on X11, Wayland, Windows, macOS and mobile alike, through SDL’s drivers); TERMINAL and HEADLESS do not deliver touch. All ten XNA gesture types are detected. On desktop Linux and Windows, SDL forwards touch-screen events from tablets and many laptop touchscreens. Android touch should be validated on a device. iOS support in this snapshot is an experimental SDL_RENDERER and METAL build/link and one-frame simulator smoke path, not broad device validation; tvOS remains unsupported.

TouchPanel.GetState()

Returns a TouchCollection containing all currently active touch contacts.

#include "Microsoft/Xna/Framework/Input/Touch/TouchPanel.hpp"
#include "Microsoft/Xna/Framework/Input/Touch/TouchCollection.hpp"
#include "Microsoft/Xna/Framework/Input/Touch/TouchLocation.hpp"
using namespace Microsoft::Xna::Framework::Input::Touch;

void Update(GameTime& gameTime) override {
    TouchCollection touches = TouchPanel::GetState();

    for (const TouchLocation& touch : touches) {
        Vector2 pos = touch.getPositionProperty(); // screen coordinates (pixels)
        int     id  = touch.getIdProperty();       // unique per contact (finger)

        switch (touch.getStateProperty()) {
            case TouchLocationState::Pressed:
                onTouchDown(id, pos);
                break;
            case TouchLocationState::Moved:
                onTouchMove(id, pos);
                break;
            case TouchLocationState::Released:
                onTouchUp(id, pos);
                break;
            case TouchLocationState::Invalid:
                // Contact lost or prediction artefact — ignore
                break;
        }
    }
}

TouchLocation members

TouchLocation is a read-only value type; as everywhere in CNA’s XNA layer its data is read through property accessors, not public fields.

// TouchLocation (read-only)
int                getIdProperty() const;        // unique ID per contact; stable while the finger is down
TouchLocationState getStateProperty() const;     // Pressed / Moved / Released / Invalid
const Vector2&     getPositionProperty() const;  // screen position in pixels
float              getPressureEXT() const;       // 0.0 - 1.0 (CNAEXT; hardware support varies)

// Try to get the state of this contact from the previous frame
bool TryGetPreviousLocation(TouchLocation& previousLocation) const;

TouchLocationState enum

enum class TouchLocationState {
    Invalid,   // 0: lost or synthetic — do not use
    Released,  // 1: finger lifted off the screen
    Pressed,   // 2: finger just made contact
    Moved,     // 3: finger is held and has moved since last frame
};

Enabling gesture recognition

Only the gesture types you enable are detected, and ReadGesture() throws System::InvalidOperationException when the queue is empty, so always guard it with getIsGestureAvailableProperty().

// Call before the first Update (e.g. in Initialize())
TouchPanel::setEnabledGesturesProperty(
    GestureType::Tap         |
    GestureType::DoubleTap   |
    GestureType::FreeDrag    |
    GestureType::Pinch       |
    GestureType::PinchComplete);

// In Update(): drain the gesture queue
while (TouchPanel::getIsGestureAvailableProperty()) {
    GestureSample gesture = TouchPanel::ReadGesture();
    processGesture(gesture);
}

GestureType enum (selected values)

GestureTypeDescription
TapQuick press and release
DoubleTapTwo rapid taps
HoldFinger held in place
FreeDragSingle-finger pan (any direction)
HorizontalDragHorizontal-only pan
VerticalDragVertical-only pan
DragCompleteFired once when drag ends
FlickFast swipe; GestureSample::getDeltaProperty() gives velocity
PinchTwo-finger spread/contract
PinchCompleteFired once when pinch ends

GestureSample members

// GestureSample (read-only accessors)
GestureType     getGestureTypeProperty() const; // which gesture was detected
TimeSpan        getTimestampProperty() const;   // when the gesture was recognised

const Vector2&  getPositionProperty() const;    // primary contact position (finger 1)
const Vector2&  getPosition2Property() const;   // secondary contact position (finger 2, for Pinch)

const Vector2&  getDeltaProperty() const;       // movement delta (drag, flick velocity)
const Vector2&  getDelta2Property() const;      // secondary delta (for Pinch)

For Pinch samples, Position and Position2 are the current positions of the two fingers. Each sample is produced by one finger moving, so only that finger’s delta (Delta for the first, Delta2 for the second) is non-zero; the previous position of a finger is therefore Position - Delta (or Position2 - Delta2).

Testing without a touch screen

By default, like XNA and FNA, TouchPanel reports only real finger events. On a desktop without touch hardware you can switch on a CNA extension that reports the left mouse button as a touch (press begins a touch, dragging moves it, release ends it); the synthesized touch goes through the same path as a real one, so GetState() and the gesture recognizer see it. Single-finger gestures such as Tap, FreeDrag and Flick can be tried this way; Pinch needs two real fingers.

// CNAEXT, off by default: never enable it in code that must match XNA exactly
TouchPanel::setMouseTouchEmulationEnabledEXT(true);

TouchPanelCapabilities caps = TouchPanel::GetCapabilities();
bool hasTouch = caps.getIsConnectedProperty();   // real touch hardware reported by the platform
int  maxTouch = caps.getMaximumTouchCountProperty();

While a Guide message box or keyboard prompt is on screen, CNA raises TouchPanel::getInputSuppressedEXT() and touch input is withheld from your game (and queued gestures are discarded), as a real XNA platform’s shell overlay would do.

Code example: drag gesture and pinch-to-zoom

class TouchDemo final : public Game {
public:
    TouchDemo() : graphics_(this) {}

protected:
    void Initialize() override {
        Game::Initialize();

        // Enable the gestures we need
        TouchPanel::setEnabledGesturesProperty(
            GestureType::FreeDrag    |
            GestureType::DragComplete|
            GestureType::Pinch       |
            GestureType::PinchComplete);

        cameraOffset_ = Vector2::Zero;
        zoom_         = 1.0f;
    }

    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
    }

    void Update(GameTime& gameTime) override {
        Game::Update(gameTime);

        // 1. Raw touch for individual finger tracking
        TouchCollection touches = TouchPanel::GetState();
        for (const auto& t : touches) {
            if (t.getStateProperty() == TouchLocationState::Pressed)
                lastTouchCount_ = static_cast<int>(touches.getCountProperty());
        }

        // 2. High-level gestures
        while (TouchPanel::getIsGestureAvailableProperty()) {
            GestureSample gs = TouchPanel::ReadGesture();

            switch (gs.getGestureTypeProperty()) {
                case GestureType::FreeDrag:
                    // Pan the "camera" offset
                    cameraOffset_ = cameraOffset_ + gs.getDeltaProperty();
                    break;

                case GestureType::DragComplete:
                    // Could apply momentum / deceleration here
                    break;

                case GestureType::Pinch: {
                    // Pinch scale from current vs previous finger distance. Position/Position2
                    // are the CURRENT finger positions; subtract the deltas for the previous ones.
                    Vector2 currDiff = gs.getPositionProperty() - gs.getPosition2Property();
                    Vector2 prevDiff = (gs.getPositionProperty()  - gs.getDeltaProperty())
                                     - (gs.getPosition2Property() - gs.getDelta2Property());

                    float prevDist = prevDiff.Length();
                    float currDist = currDiff.Length();

                    if (prevDist > 1.0f) {
                        float scale = currDist / prevDist;
                        zoom_ = MathHelper::Clamp(zoom_ * scale, 0.2f, 5.0f);
                    }
                    break;
                }

                case GestureType::PinchComplete:
                    // Snap zoom to nearest 0.5 step (optional)
                    zoom_ = std::round(zoom_ * 2.0f) / 2.0f;
                    break;

                default:
                    break;
            }
        }
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::CornflowerBlue);

        // Apply pan + zoom via a SpriteBatch transform
        Matrix transform = Matrix::CreateTranslation(cameraOffset_.X,
                                                     cameraOffset_.Y, 0)
                         * Matrix::CreateScale(zoom_);
        spriteBatch_->Begin(SpriteSortMode::Deferred,
                             BlendState::AlphaBlend, nullptr, nullptr, nullptr,
                             nullptr, transform);
        // ... draw game world ...
        spriteBatch_->End();
        // No Present(): Game presents the frame after Draw() returns.
    }

private:
    GraphicsDeviceManager            graphics_;
    std::unique_ptr<SpriteBatch>     spriteBatch_;
    Vector2                          cameraOffset_;
    float                            zoom_           = 1.0f;
    int                              lastTouchCount_ = 0;
};