virtual_gamepad_pro 0.3.1
virtual_gamepad_pro: ^0.3.1 copied to clipboard
Advanced virtual controller suite: joystick, d-pad, buttons, and runtime layout editor with JSON serialization. Optimized for remote play.
Virtual Gamepad Pro #
Live demo (runs this repo's example/): plugin.qianpc.cn/gamepad
A pure-Flutter virtual controller suite (Joystick / D-Pad / Buttons / Mouse / Keyboard / Macros) with a runtime layout editor.
This package cleanly separates:
- Definition: bindings, styles, semantics (owned by your app code)
- State: editable runtime data like position/size/opacity/config (portable JSON)
So you can:
- Share and persist only the minimal JSON state (no callbacks, no business logic, no platform objects)
- Keep bindings and visual rules strongly typed (no implicit
String + Mapconventions) - Control performance (less dynamic inference, fewer runtime maps)
Coordinate system: every control uses normalized percentage coordinates (0.0–1.0) for both position and size, so layouts are resolution-independent.
Highlights #
- Overlay renderer: render
definition + statewith predictable performance - Runtime layout editor: drag / resize / opacity; saves a minimal
VirtualControllerStateJSON - Strongly-typed input:
InputBindingfor keyboard & gamepad; supports registering custom buttons - Theme layer:
VirtualControlThemecan override style/label/config (and other visual props) at render-time without mutating original data - Macro suite: record, edit, and serialize an
InputEventsequence to power macro buttons
Screenshots #
| Layout Editor | Macro Editor |
|---|---|
![]() |
![]() |
| Macro Recorder (dock) | Keyboard Control |
|---|---|
![]() |
![]() |
Gamepad Buttons
Editor Signals (examples)
| Button Edit Signal | Joystick Edit Signal |
|---|---|
![]() |
![]() |
Install #
dependencies:
virtual_gamepad_pro: ^0.3.0
Quick Start (Render Overlay: definition + state) #
Recommended data model:
VirtualControllerLayout: control definition (binding/style/default layout, owned by code)VirtualControllerState: user-editable state (layout/opacity/config only, JSON-friendly)
import 'package:flutter/material.dart';
import 'package:virtual_gamepad_pro/virtual_gamepad_pro.dart';
class GamePage extends StatelessWidget {
const GamePage({super.key});
@override
Widget build(BuildContext context) {
final definition = VirtualControllerLayout(
schemaVersion: 1,
name: 'Default',
controls: [
VirtualJoystick(
id: 'ls',
label: 'LS',
layout: const ControlLayout(x: 0.1, y: 0.6, width: 0.2, height: 0.2),
trigger: TriggerType.hold,
mode: JoystickMode.gamepad,
stickType: GamepadStickId.left,
),
VirtualButton(
id: 'btn_a',
label: 'A',
layout: const ControlLayout(x: 0.8, y: 0.7, width: 0.1, height: 0.1),
trigger: TriggerType.tap,
binding: const GamepadButtonBinding(GamepadButtonId.a),
),
],
);
final state = const VirtualControllerState(schemaVersion: 1, controls: []);
return Scaffold(
body: Stack(
children: [
const Center(child: Text('Game Content')),
VirtualControllerOverlay(
definition: definition,
state: state,
onInputEvent: (event) {
if (event is GamepadAxisInputEvent) {
debugPrint('Axis ${event.axisId}: ${event.x}, ${event.y}');
} else if (event is GamepadButtonInputEvent) {
debugPrint('Button ${event.button}: ${event.isDown}');
} else if (event is KeyboardInputEvent) {
debugPrint('Key ${event.key}: ${event.isDown}');
}
},
),
],
),
);
}
}
Example App (for pub.dev) #
This repository includes a complete example app (layout management + runtime editor + macro entry points):
- Directory:
example/ - Entry:
example/lib/main.dart
Theming (Recommended): VirtualControlTheme #
Themes are meant to decorate at render-time, not to rewrite your source data.
You can treat a theme as a pure function: VirtualControl -> VirtualControl.
Notes:
- The overlay resolves position/size from
state.layout(ordefinition.layout) and uses it for geometry. Do not rely on themes to move/resize controls; changestate(or definition defaults) instead. - Themes run after
state.confighas been merged into each control'sconfig, so you can override config-driven visuals insidedecorate(...). - Do not change
control.idinside themes (state lookup and editor selection are ID-based). VirtualKeyClusteris decorated once as a cluster, then expanded into keys; expanded keys are decorated again.
final theme = RuleBasedVirtualControlTheme(
base: const DefaultVirtualControlTheme(),
post: [
ControlRule(
when: ControlMatchers.gamepadButtonId(GamepadButtonId.a),
transform: (c) => (c as VirtualButton).copyWith(
style: const ControlStyle(color: Colors.green),
),
),
],
);
VirtualControllerOverlay(
definition: definition,
state: state,
theme: theme,
onInputEvent: onInputEvent,
);
Add Custom Gamepad Buttons: InputBindingRegistry #
Register a strongly-typed custom button (e.g. Turbo / Screenshot / OEM keys) before building your layout/editor palette:
void main() {
InputBindingRegistry.registerGamepadButton(code: 'turbo', label: 'Turbo');
InputBindingRegistry.registerGamepadButton(code: 'screenshot', label: 'Shot');
runApp(const MyApp());
}
Notes:
- Button
codeis normalized and de-duplicated internally (so the same logical code maps to one typed ID). - When recovering controls from state-only IDs like
btn_<code>_..., the renderer may auto-register unknown codes.
Layout Serialization Guide #
Goals & constraints #
- Shareable & persistable: serialized data must not contain callbacks, platform objects, or business semantics
- Cross-device reuse: coordinates are normalized (0.0–1.0), avoiding resolution coupling
- Evolvable:
schemaVersionsupports structure evolution
Two-layer model: Definition vs State #
- Definition:
VirtualControllerLayout(control types, bindings, styles, default layout) - State:
VirtualControllerState(user-editable layout/opacity/config)
Core rule: Definition is controlled by code; State is the minimal data you persist/share.
Minimal State JSON (recommended) #
{
"schemaVersion": 1,
"name": "My Layout",
"controls": [
{
"id": "btn_a",
"layout": { "x": 0.78, "y": 0.63, "width": 0.12, "height": 0.12 },
"opacity": 0.7
}
]
}
Render-time merge behavior (important) #
- For every control in
definition, the renderer:- Picks
layout = state.layout ?? definition.layoutandopacity = state.opacity ?? 1.0 - Merges
state.configintocontrol.config(state overrides definition) - Applies extra macro fields:
config['label'](non-empty) overrides label;config['sequence'](legacy) overridesVirtualMacroButton.sequence - Applies
theme.decorate(...)on the merged control
- Picks
- If
statecontains control IDs that do not exist indefinition, the renderer may attempt best-effort completion by ID prefix (useful for migration/replay). - While loading JSON, any state entry with
config.deleted == trueis ignored.
Dynamic control IDs (best-effort recovery) #
When a VirtualControlState.id is not found in definition, the overlay may create a control by ID prefix (so that a shared state can still render something usable). Common prefixes include:
macro_btn_mouse_,wheel_,split_mouse_,scroll_stick_dpad_joystick_wasd_,joystick_arrows_,joystick_gamepad_left_,joystick_gamepad_right_key_
This enables sharing only state while still restoring a usable layout, with definition-level style/binding owned by your app.
API Notes #
VirtualControllerOverlay #
Renderer entry point (definition + state).
| Property | Type | Description |
|---|---|---|
definition |
VirtualControllerLayout |
Control definitions (bindings/styles/default layout). |
state |
VirtualControllerState |
Editable runtime state (layout/opacity/config), JSON-friendly. |
theme |
VirtualControlTheme |
Optional render-time decorator (styling/labels/config overrides). |
onInputEvent |
Function(InputEvent) |
Input event callback. |
opacity |
double |
Global overlay opacity (0.0–1.0). |
showLabels |
bool |
Whether to show text labels on controls. |
immersive |
bool |
Hide system UI (status/navigation) with immersive mode. |
VirtualControllerLayoutEditor #
Runtime layout editor: edits only state (position/size/opacity); does not mutate bindings/styles/actions.
| Property | Type | Description |
|---|---|---|
layoutId |
String |
Unique ID for the layout being edited. |
loadDefinition |
Future<VirtualControllerLayout> Function(id) |
Load definition (owned by code). |
loadState |
Future<VirtualControllerState> Function(id) |
Load state (JSON). |
saveState |
Future<void> Function(id, state) |
Persist state (JSON). |
previewDecorator |
Function |
Optional hook to decorate preview (e.g. apply theme). |
onClose |
VoidCallback? |
Called when the user taps the close button. |
readOnly |
bool |
Read-only mode (viewing only). |
allowAddRemove |
bool |
Whether adding/removing controls is allowed. |
allowResize |
bool |
Whether resizing controls is allowed. |
allowMove |
bool |
Whether moving controls is allowed. |
allowRename |
bool |
Whether renaming the layout is allowed. |
enabledPaletteTabs |
Set<VirtualControllerEditorPaletteTab> |
Which palette tabs are shown. |
initialPaletteTab |
VirtualControllerEditorPaletteTab |
Initially selected palette tab. |
immersive |
bool |
Immersive mode. |
Macro recording & editing #
For normal runtime rendering you only need VirtualControllerOverlay. Macro workflows are opt-in tooling:
MacroSuitePage: macro editor (main editor + recording importer). Typically writes macro data into a macro button stateconfig:config['recordingV2']: timeline JSONconfig['label']: optional display label override
VirtualControllerMacroRecordingSession: records input events (optionally mixing hardware keyboard/mouse) and returns arecordingV2timeline JSON list (each item includesatMs) for editing or storage.
recordingV2 timeline format (stored in VirtualControlState.config['recordingV2'])
Each item is a JSON object:
atMs(int): timestamp from the start of playback (milliseconds)type(string): event kinddata(object): payload (depends ontype)
Supported type values and required data keys:
keyboard:key(string),isDown(bool), optionalmodifiers(string[])mouse_button:button(string),isDown(bool)mouse_wheel:direction(string),delta(int)mouse_wheel_vector:dx(double),dy(double)gamepad_button:button(string),isDown(bool)gamepad_axis:axisId(string,left/right),x(double),y(double)joystick:dx(double),dy(double),activeKeys(string[])custom:id(string), optionaldata(object)
Type Reference (Complete) #
Input events (InputEvent)
All events delivered via onInputEvent are one of:
KeyboardInputEvent:key: KeyboardKey,isDown: bool,modifiers: List<KeyboardKey>MouseButtonInputEvent:button: MouseButtonId,isDown: boolMouseWheelInputEvent:direction: MouseWheelDirection,delta: intMouseWheelVectorInputEvent:dx: double,dy: doubleJoystickInputEvent:dx: double,dy: double,activeKeys: List<KeyboardKey>GamepadButtonInputEvent:button: GamepadButtonId,isDown: boolGamepadAxisInputEvent:axisId: GamepadStickId,x: double,y: doubleCustomInputEvent:id: String,data: Map<String, dynamic>MacroInputEvent:sequence: List<TimedInputEvent>(in-memory only; not stored as a single timeline item)
Identifiers / enums (identifiers.dart)
MouseButtonId:left,right,middleMouseWheelDirection:up,downJoystickMode:keyboard,gamepadGamepadStickId:left,rightGamepadAxisId:left_x,left_y,right_x,right_y
Bindings (InputBinding)
Serialized binding types:
keyboard:KeyboardBinding(key: KeyboardKey, modifiers: List<KeyboardKey>)gamepad_button:GamepadButtonBinding(GamepadButtonId)
Triggers (TriggerType)
JSON string values:
tapholddouble_tap
Controls (VirtualControl)
Controls that can exist in a VirtualControllerLayout.controls list:
VirtualJoystickVirtualDpadVirtualButtonVirtualKeyVirtualKeyClusterVirtualMouseButtonVirtualMouseWheelVirtualSplitMouseVirtualScrollStickVirtualMacroButtonVirtualCustomControl
Gamepad button IDs (GamepadButtonId)
Built-in code values:
a,b,x,ylb,rb,lt,rtl1,l2,r1,r2back,start,view,menu,options,sharedpad_up,dpad_down,dpad_left,dpad_rightl3,r3triangle,circle,square,cross
ControlStyle #
Visual appearance of a control.
| Property | Type | Description |
|---|---|---|
shape |
BoxShape |
circle or rectangle. |
color |
Color? |
Background color. |
borderColor |
Color? |
Border color. |
lockedColor |
Color? |
Color for "locked" state (e.g. joystick lock). |
backgroundImagePath |
String? |
Asset path or URL for background image. |
shadows |
List<BoxShadow> |
Shadow list for neon/glow effects. |
imageFit |
BoxFit |
How the image should be inscribed. |
VirtualJoystick #
A virtual thumbstick.
| Property | Type | Description |
|---|---|---|
deadzone |
double |
Minimum input value to register (0.0–1.0). Default: 0.1. |
mode |
JoystickMode |
Keyboard (WASD-like) or gamepad stick mode. |
stickType |
GamepadStickId |
left or right (used by GamepadAxisInputEvent.axisId). |
keys |
List<KeyboardKey> |
Up/Left/Down/Right keys for keyboard mode. |
axes |
List<GamepadAxisId> |
Axis identifiers (mainly for modeling/compat). |
VirtualButton #
A standard push button.
| Property | Type | Description |
|---|---|---|
trigger |
TriggerType |
tap (press/release), hold (continuous), doubleTap. |
label |
String |
Text displayed on the button. |
binding |
InputBinding |
Strong-typed binding for emitted input. |
Ultra strong typed helper
final GamepadButtonId id = button.gamepadButton; // throws if not gamepad
final GamepadButtonId? maybe = button.gamepadButtonOrNull;
Layout Editor Integration #
To use the editor, implement the persistence layer (load/save).
// Example using SharedPreferences
Future<void> saveState(String id, VirtualControllerState state) async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = jsonEncode(state.toJson());
await prefs.setString('layout_state_$id', jsonStr);
}
Future<VirtualControllerState> loadState(String id) async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString('layout_state_$id');
if (jsonStr == null) {
return const VirtualControllerState(schemaVersion: 1, controls: []);
}
return VirtualControllerState.fromJson(jsonDecode(jsonStr));
}
Future<VirtualControllerLayout> loadDefinition(String id) async {
return VirtualControllerLayout.xbox();
}
// In your Widget:
VirtualControllerLayoutEditor(
layoutId: 'user_custom_1',
loadDefinition: loadDefinition,
loadState: loadState,
saveState: saveState,
)
Notes:
- The editor palette automatically lists registered custom buttons.
License #
MIT License. See LICENSE for details.
Built in Production #
This package was extracted from a real production need: a highly customizable, editable, serializable (shareable) virtual controller + macro system with predictable performance. If you're building game streaming, remote control, cloud apps, or tool-like products, it should help you ship interaction faster.
- Company: Hangzhou iLingJing Technology Co., Ltd.
- Product: QianPC
- Author: liliin.icu





