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 startTutorial 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.

Platform notes. This example assumes the default SDL3 platform implementation; platform selection is independent of the graphics renderer. 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 alpha.1 is a narrow SDL_RENDERER 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.Position; // screen coordinates (pixels)
        int     id  = touch.Id;       // 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 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::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.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::setEnabledGesturesProperty(
            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.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: {
                    // Compute pinch scale from current vs previous finger distance
                    Vector2 prevDiff = gs.getPositionProperty()  - gs.getPosition2Property();
                    Vector2 currDiff = (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();

        gd.Present();
    }

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