Skip to content

Commit cfac5aa

Browse files
committed
Add support for automatically attaching to views in Android
1 parent 31ca086 commit cfac5aa

File tree

12 files changed

+253
-84
lines changed

12 files changed

+253
-84
lines changed

engine/Orbit.Input/GameControllerManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public partial class GameControllerManager
88

99
public static GameControllerManager Current => current ?? (current = new GameControllerManager());
1010

11-
public partial Task Initialize();
11+
public partial Task StartDiscovery();
1212

1313
public IReadOnlyCollection<GameController> GameControllers => gameControllers;
1414

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace Orbit.Input;
2+
3+
public partial class GameControllerOptions
4+
{
5+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Microsoft.Maui.LifecycleEvents;
2+
3+
namespace Orbit.Input;
4+
5+
public static class MauiAppBuilderExtensions
6+
{
7+
// public static MauiAppBuilder AddAudio(
8+
// this MauiAppBuilder mauiAppBuilder,
9+
// Action<AudioPlayerOptions>? configurePlaybackOptions = null,
10+
// Action<AudioRecorderOptions>? configureRecordingOptions = null)
11+
// {
12+
// var playbackOptions = new AudioPlayerOptions();
13+
// configurePlaybackOptions?.Invoke(playbackOptions);
14+
// AudioManager.Current.DefaultPlayerOptions = playbackOptions;
15+
//
16+
// var recordingOptions = new AudioRecorderOptions();
17+
// configureRecordingOptions?.Invoke(recordingOptions);
18+
// AudioManager.Current.DefaultRecorderOptions = recordingOptions;
19+
//
20+
// mauiAppBuilder.Services.AddSingleton(AudioManager.Current);
21+
//
22+
// return mauiAppBuilder;
23+
// }
24+
25+
public static MauiAppBuilder UseOrbitInput(
26+
this MauiAppBuilder builder,
27+
Action<GameControllerOptions>? configureControllerOptions = null)
28+
{
29+
builder.ConfigureLifecycleEvents(appLifecycle =>
30+
{
31+
#if ANDROID
32+
appLifecycle.AddAndroid((android) =>
33+
{
34+
bool appCreated = false;
35+
36+
android.OnCreate((activity, bundle) =>
37+
{
38+
if (!appCreated)
39+
{
40+
var controllerOptions = new GameControllerOptions();
41+
configureControllerOptions?.Invoke(controllerOptions);
42+
43+
appCreated = true;
44+
45+
if (controllerOptions.AutoAttachToLifecycleEvents)
46+
{
47+
GameControllerManager.Current.AttachToCurrentActivity(activity);
48+
}
49+
50+
// TODO: Do the same for keyboard later
51+
// do we need to do anything to allow them to play nicely together?
52+
}
53+
});
54+
});
55+
#endif
56+
});
57+
58+
return builder;
59+
}
60+
}

engine/Orbit.Input/Platforms/Android/GameController.cs

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ namespace Orbit.Input;
55
// All the code in this file is only included on Android.
66
public partial class GameController
77
{
8+
private readonly int deviceId;
9+
810
public GameController(int deviceId)
911
{
12+
this.deviceId = deviceId;
13+
1014
Dpad = new Stick(this, nameof(Dpad));
1115
LeftStick = new Stick(this, nameof(LeftStick));
1216
RightStick = new Stick(this, nameof(RightStick));
@@ -15,8 +19,13 @@ public GameController(int deviceId)
1519
RightShoulder = new Shoulder(this, nameof(RightShoulder));
1620
}
1721

18-
public void OnGenericMotionEvent(MotionEvent motionEvent)
22+
public bool OnGenericMotionEvent(MotionEvent motionEvent)
1923
{
24+
if (motionEvent.DeviceId != this.deviceId)
25+
{
26+
return false;
27+
}
28+
2029
// Check that the event came from a game controller
2130
if (motionEvent.Source.HasFlag(InputSourceType.Joystick) &&
2231
motionEvent.Action == MotionEventActions.Move)
@@ -35,55 +44,20 @@ public void OnGenericMotionEvent(MotionEvent motionEvent)
3544
// Process the current movement sample in the batch (position -1)
3645
ProcessJoystickInput(motionEvent, -1);
3746
}
47+
48+
return true;
3849
}
39-
40-
private static float GetCenteredAxis(MotionEvent motionEvent, InputDevice device, Axis axis, int historyPos)
41-
{
42-
var range = device.GetMotionRange(axis, motionEvent.Source);
4350

44-
// A joystick at rest does not always report an absolute position of
45-
// (0,0). Use the getFlat() method to determine the range of values
46-
// bounding the joystick axis center.
47-
if (range is not null)
48-
{
49-
var flat = range.Flat;
50-
var value = historyPos < 0 ? motionEvent.GetAxisValue(axis):
51-
motionEvent.GetHistoricalAxisValue(axis, historyPos);
52-
53-
// Ignore axis values that are within the 'flat' region of the
54-
// joystick axis center.
55-
if (Math.Abs(value) > flat)
56-
{
57-
return value;
58-
}
59-
}
60-
return 0;
61-
}
62-
63-
private void ProcessJoystickInput(MotionEvent motionEvent, int historyPos)
51+
public bool OnKeyDown(InputEvent inputEvent)
6452
{
65-
var inputDevice = motionEvent.Device;
66-
67-
if (inputDevice is null)
53+
if (inputEvent.DeviceId != this.deviceId)
6854
{
69-
return;
55+
return false;
7056
}
71-
72-
LeftStick.XAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.X, historyPos);
73-
LeftStick.YAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.Y, historyPos);
74-
75-
RightStick.XAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.Z, historyPos);
76-
RightStick.YAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.Rz, historyPos);
7757

78-
LeftShoulder.Trigger = GetCenteredAxis(motionEvent, inputDevice, Axis.Ltrigger, historyPos);
79-
RightShoulder.Trigger = GetCenteredAxis(motionEvent, inputDevice, Axis.Rtrigger, historyPos);
80-
}
81-
82-
public void OnKeyDown(InputEvent inputEvent)
83-
{
8458
if (!IsDpadDevice(inputEvent))
8559
{
86-
return;
60+
return false;
8761
}
8862

8963
switch (inputEvent)
@@ -121,13 +95,20 @@ public void OnKeyDown(InputEvent inputEvent)
12195
RightShoulder.Button = true;
12296
break;
12397
}
98+
99+
return true;
124100
}
125101

126-
public void OnKeyUp(InputEvent inputEvent)
102+
public bool OnKeyUp(InputEvent inputEvent)
127103
{
104+
if (inputEvent.DeviceId != this.deviceId)
105+
{
106+
return false;
107+
}
108+
128109
if (!IsDpadDevice(inputEvent))
129110
{
130-
return;
111+
return false;
131112
}
132113

133114
switch (inputEvent)
@@ -165,11 +146,55 @@ public void OnKeyUp(InputEvent inputEvent)
165146
RightShoulder.Button = false;
166147
break;
167148
}
149+
150+
return true;
151+
}
152+
153+
private static float GetCenteredAxis(MotionEvent motionEvent, InputDevice device, Axis axis, int historyPos)
154+
{
155+
var range = device.GetMotionRange(axis, motionEvent.Source);
156+
157+
// A joystick at rest does not always report an absolute position of
158+
// (0,0). Use the getFlat() method to determine the range of values
159+
// bounding the joystick axis center.
160+
if (range is not null)
161+
{
162+
var flat = range.Flat;
163+
var value = historyPos < 0 ? motionEvent.GetAxisValue(axis):
164+
motionEvent.GetHistoricalAxisValue(axis, historyPos);
165+
166+
// Ignore axis values that are within the 'flat' region of the
167+
// joystick axis center.
168+
if (Math.Abs(value) > flat)
169+
{
170+
return value;
171+
}
172+
}
173+
return 0;
168174
}
169175

170176
private static bool IsDpadDevice(InputEvent inputEvent)
171177
{
172178
// Check that input comes from a device with directional pads.
173179
return inputEvent.Source.HasFlag(InputSourceType.Dpad);
174180
}
181+
182+
private void ProcessJoystickInput(MotionEvent motionEvent, int historyPos)
183+
{
184+
var inputDevice = motionEvent.Device;
185+
186+
if (inputDevice is null)
187+
{
188+
return;
189+
}
190+
191+
LeftStick.XAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.X, historyPos);
192+
LeftStick.YAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.Y, historyPos);
193+
194+
RightStick.XAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.Z, historyPos);
195+
RightStick.YAxis = GetCenteredAxis(motionEvent, inputDevice, Axis.Rz, historyPos);
196+
197+
LeftShoulder.Trigger = GetCenteredAxis(motionEvent, inputDevice, Axis.Ltrigger, historyPos);
198+
RightShoulder.Trigger = GetCenteredAxis(motionEvent, inputDevice, Axis.Rtrigger, historyPos);
199+
}
175200
}
Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
1+
using Android.App;
12
using Android.Views;
23

34
namespace Orbit.Input;
45

56
public partial class GameControllerManager
67
{
8+
private KeyListener? keyListener;
9+
private GenericMotionListener? genericMotionListener;
10+
711
private GameControllerManager()
812
{
913
}
14+
15+
public void AttachToCurrentActivity(Activity activity)
16+
{
17+
keyListener = new KeyListener(activity, (code, e) =>
18+
{
19+
switch (e.Action)
20+
{
21+
case KeyEventActions.Down when OnKeyDown(code, e):
22+
case KeyEventActions.Up when OnKeyUp(code, e):
23+
return true;
24+
}
25+
26+
return false;
27+
});
28+
29+
genericMotionListener = new GenericMotionListener(activity, OnGenericMotionEvent);
30+
}
1031

11-
public partial Task Initialize()
32+
public partial Task StartDiscovery()
1233
{
1334
var deviceIds = InputDevice.GetDeviceIds();
1435

@@ -37,45 +58,18 @@ public partial Task Initialize()
3758
return Task.CompletedTask;
3859
}
3960

40-
public void OnGenericMotionEvent(MotionEvent? e)
61+
public bool OnGenericMotionEvent(MotionEvent? e)
4162
{
42-
if (e is null)
43-
{
44-
return;
45-
}
46-
47-
// TODO: thread safety?
48-
foreach (var controller in gameControllers)
49-
{
50-
controller.OnGenericMotionEvent(e);
51-
}
63+
return e is not null && gameControllers.Any(controller => controller.OnGenericMotionEvent(e));
5264
}
5365

54-
public void OnKeyDown(Keycode keyCode, KeyEvent? e)
66+
public bool OnKeyDown(Keycode keyCode, KeyEvent? e)
5567
{
56-
if (e is null)
57-
{
58-
return;
59-
}
60-
61-
// TODO: thread safety?
62-
foreach (var controller in gameControllers)
63-
{
64-
controller.OnKeyDown(e);
65-
}
68+
return e is not null && gameControllers.Any(controller => controller.OnKeyDown(e));
6669
}
6770

68-
public void OnKeyUp(Keycode keyCode, KeyEvent? e)
71+
public bool OnKeyUp(Keycode keyCode, KeyEvent? e)
6972
{
70-
if (e is null)
71-
{
72-
return;
73-
}
74-
75-
// TODO: thread safety?
76-
foreach (var controller in gameControllers)
77-
{
78-
controller.OnKeyUp(e);
79-
}
73+
return e is not null && gameControllers.Any(controller => controller.OnKeyUp(e));
8074
}
81-
}
75+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Orbit.Input;
2+
3+
public partial class GameControllerOptions
4+
{
5+
public bool AutoAttachToLifecycleEvents { get; set; } = true;
6+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Android.App;
2+
using Android.Views;
3+
4+
using View = Android.Views.View;
5+
6+
namespace Orbit.Input;
7+
8+
public class GenericMotionListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalFocusChangeListener, View.IOnGenericMotionListener
9+
{
10+
private readonly Activity activity;
11+
private readonly Func<MotionEvent, bool> callback;
12+
13+
public GenericMotionListener(Activity activity, Func<MotionEvent, bool> callback)
14+
{
15+
this.callback = callback ?? throw new ArgumentNullException(nameof(callback));
16+
this.activity = activity;
17+
this.activity.Window?.DecorView.ViewTreeObserver?.AddOnGlobalFocusChangeListener(this);
18+
}
19+
20+
public void OnGlobalFocusChanged(View? oldFocus, View? newFocus)
21+
{
22+
oldFocus?.SetOnGenericMotionListener(null);
23+
24+
newFocus?.SetOnGenericMotionListener(this);
25+
}
26+
27+
public bool OnGenericMotion(View? v, MotionEvent? e)
28+
{
29+
// Only return if something handles it
30+
return e is not null && callback.Invoke(e);
31+
}
32+
}

0 commit comments

Comments
 (0)