Tutorial 49: Touch Input and Gestures
What you’ll learn
- Polling
TouchPanel.GetState()and reading aTouchLocation. - The
TouchLocationStatelifecycle 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)
| GestureType | Description |
|---|---|
Tap | Quick press and release |
DoubleTap | Two rapid taps |
Hold | Finger held in place |
FreeDrag | Single-finger pan (any direction) |
HorizontalDrag | Horizontal-only pan |
VerticalDrag | Vertical-only pan |
DragComplete | Fired once when drag ends |
Flick | Fast swipe; GestureSample::getDeltaProperty() gives velocity |
Pinch | Two-finger spread/contract |
PinchComplete | Fired 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;
};
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Touch panel and gesture semantics — When CNA's touch state advances, which connected flag to trust, TouchCollection and TouchLocation contracts, gesture filtering, timestamps and the deliberate differences from FNA.