gamepads 0.1.12 copy "gamepads: ^0.1.12" to clipboard
gamepads: ^0.1.12 copied to clipboard

A Flutter plugin to handle gamepad input across multiple platforms.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:gamepads/gamepads.dart';

import 'package:gamepads_example/share_log.dart'
    if (dart.library.js_interop) 'package:gamepads_example/share_log_web.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Gamepads Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

/// Wraps either a normalized event or a raw event that could not be
/// normalized.
class _EventEntry {
  final NormalizedGamepadEvent? normalized;
  final GamepadEvent raw;

  _EventEntry({required this.raw, this.normalized});
}

class _MyHomePageState extends State<MyHomePage> {
  StreamSubscription<GamepadEvent>? _subscription;
  StreamSubscription<GamepadConnectionEvent>? _connectionSubscription;
  late final GamepadNormalizer _normalizer;

  List<GamepadController> _gamepads = [];
  List<_EventEntry> _lastEvents = [];
  final List<String> _eventLog = [];
  final Map<String, String> _gamepadNames = {};
  bool loading = false;

  Future<void> _listGamepads() async {
    setState(() => loading = true);
    final response = await Gamepads.list();
    for (final gamepad in response) {
      _gamepadNames[gamepad.id] = gamepad.name;
    }
    final previous = _gamepads;
    setState(() {
      _gamepads = response;
      loading = false;
    });
    for (final gamepad in previous) {
      await gamepad.dispose();
    }
  }

  void _clear() {
    setState(() {
      _lastEvents = [];
      _eventLog.clear();
    });
  }

  Future<void> _shareLog() async {
    if (_eventLog.isEmpty) {
      return;
    }
    await shareLog(_eventLog);
  }

  @override
  void initState() {
    super.initState();
    _normalizer = GamepadNormalizer();
    _connectionSubscription = Gamepads.connectionEvents.listen((event) {
      _eventLog.add(
        '${DateTime.now().toIso8601String()} '
        '[${event.name}] ${event.type.name}',
      );
      _listGamepads();
    });
    _subscription = Gamepads.events.listen((event) {
      if (!_gamepadNames.containsKey(event.gamepadId)) {
        _listGamepads();
      }
      final normalized = _normalizer.normalize(event);
      final timestamp = DateTime.now().toIso8601String();
      final name = _gamepadNames[event.gamepadId];
      final device =
          name ??
          'vendor:${event.vendorId ?? "?"} '
              'product:${event.productId ?? "?"}';
      if (normalized.isEmpty) {
        _eventLog.add(
          '$timestamp [$device] [unmapped] ${event.type.name}: '
          '${event.key} = ${event.value}',
        );
      } else {
        for (final n in normalized) {
          final label = n.button != null
              ? '${n.button} = ${n.value}'
              : '${n.axis} = ${n.value.toStringAsFixed(2)}';
          _eventLog.add(
            '$timestamp [$device] $label '
            '(raw: ${event.key} ${event.value})',
          );
        }
      }
      setState(() {
        final newEntries = <_EventEntry>[
          if (normalized.isEmpty)
            _EventEntry(raw: event)
          else
            for (final normalizedEvent in normalized)
              _EventEntry(
                normalized: normalizedEvent,
                raw: event,
              ),
          ..._lastEvents,
        ];
        if (newEntries.length > 8) {
          newEntries.removeRange(8, newEntries.length);
        }
        _lastEvents = newEntries;
      });
    });
  }

  @override
  void dispose() {
    for (final gamepad in _gamepads) {
      unawaited(gamepad.stopRumble());
    }
    _subscription?.cancel();
    _connectionSubscription?.cancel();
    for (final gamepad in _gamepads) {
      unawaited(gamepad.dispose());
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Gamepads Example'),
      ),
      body: Center(
        child: SingleChildScrollView(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Events:',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              ..._lastEvents.map(_buildEventTile),
              TextButton(
                onPressed: _clear,
                child: const Text('Clear Events'),
              ),
              TextButton(
                onPressed: _eventLog.isEmpty ? null : _shareLog,
                child: const Text('Share Log'),
              ),
              const SizedBox(height: 16),
              TextButton(
                onPressed: _listGamepads,
                child: const Text('List Gamepads'),
              ),
              const Text(
                'Gamepads:',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                ),
              ),
              if (loading)
                const CircularProgressIndicator()
              else ...[
                for (final gamepad in _gamepads) ...[
                  Text(_describeGamepad(gamepad)),
                  Wrap(
                    children: [
                      TextButton(
                        onPressed: () async {
                          final supported = await gamepad.hasRumble();
                          final accepted =
                              supported &&
                              await gamepad.rumble(
                                lowFrequency: 0.5,
                                highFrequency: 0.5,
                              );
                          if (!mounted) {
                            return;
                          }
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text(
                                accepted
                                    ? 'Rumble requested (500 ms)'
                                    : 'Rumble unavailable for this controller',
                              ),
                            ),
                          );
                        },
                        child: const Text('Test rumble'),
                      ),
                      TextButton(
                        onPressed: gamepad.stopRumble,
                        child: const Text('Stop rumble'),
                      ),
                    ],
                  ),
                ],
              ],
            ],
          ),
        ),
      ),
    );
  }

  String _describeGamepad(GamepadController gamepad) {
    final vendorId = gamepad.vendorId;
    final productId = gamepad.productId;

    if (vendorId == null || productId == null) {
      return '${gamepad.id} - ${gamepad.name}';
    }

    final vendor = vendorId.toRadixString(16).padLeft(4, '0');
    final product = productId.toRadixString(16).padLeft(4, '0');

    return '${gamepad.id} - ${gamepad.name} ($vendor:$product)';
  }

  Widget _buildEventTile(_EventEntry entry) {
    final normalized = entry.normalized;
    final raw = entry.raw;

    if (normalized != null) {
      final label = normalized.button != null
          ? '${normalized.button} = '
                '${normalized.value}'
          : '${normalized.axis} = '
                '${normalized.value.toStringAsFixed(2)}';
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 2),
        child: Text(
          '$label  (raw: ${raw.key} ${raw.value})',
        ),
      );
    }

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Text(
        '[unmapped] ${raw.type.name}: '
        '${raw.key} = ${raw.value}',
        style: const TextStyle(
          color: Colors.grey,
          fontStyle: FontStyle.italic,
        ),
      ),
    );
  }
}