Tutorial 49: Touch Input and Gestures

CNA — C++ XNA 4.0 reimplementation

Platform notes. CNA's touch input is routed through the SDL3 backend. The API surface is implemented, but full hardware validation on Android and iOS touch panels is pending. On desktop Linux/Windows, SDL3 can forward touch-screen events from tablets and some laptop touchscreens. Test on real hardware before shipping.

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.Position; // screen coordinates (pixels)
        int     id  = touch.Id;       // unique per contact (finger)

        switch (touch.State) {
            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 struct

// TouchLocation fields (read-only)
struct TouchLocation {
    int                Id;        // Unique ID per contact; stable while finger is down
    TouchLocationState State;     // Pressed / Moved / Released / Invalid
    Vector2            Position;  // Screen position in pixels
    float              Pressure;  // 0.0 – 1.0 (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,   // lost or synthetic — do not use
    Moved,     // finger is held and has moved since last frame
    Pressed,   // finger just made contact
    Released,  // finger lifted off the screen
};

Enabling gesture recognition

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

// In Update(): drain the gesture queue
while (TouchPanel::IsGestureAvailable()) {
    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.Delta gives velocity
PinchTwo-finger spread/contract
PinchCompleteFired once when pinch ends

GestureSample fields

struct GestureSample {
    GestureType GestureType; // which gesture was detected

    TimeSpan    Timestamp;   // when the gesture was recognised

    Vector2     Position;    // primary contact position (finger 1)
    Vector2     Position2;   // secondary contact position (finger 2, for Pinch)

    Vector2     Delta;       // movement delta for this frame (drag, flick velocity)
    Vector2     Delta2;      // secondary delta (for Pinch)
};

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::EnabledGestures =
            GestureType::FreeDrag    |
            GestureType::DragComplete|
            GestureType::Pinch       |
            GestureType::PinchComplete;

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

    void Update(GameTime& gameTime) override {
        // 1. Raw touch for individual finger tracking
        TouchCollection touches = TouchPanel::GetState();
        for (const auto& t : touches) {
            if (t.State == TouchLocationState::Pressed)
                lastTouchCount_ = static_cast<int>(touches.Count());
        }

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

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

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

                case GestureType::Pinch: {
                    // Compute pinch scale from current vs previous finger distance
                    Vector2 prevDiff = gs.Position  - gs.Position2;
                    Vector2 currDiff = (gs.Position + gs.Delta)
                                     - (gs.Position2 + gs.Delta2);

                    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,
                             nullptr, nullptr, nullptr, nullptr,
                             nullptr, transform);
        // ... draw game world ...
        spriteBatch_->End();

        gd.Present();
    }

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