diff --git a/osu.Framework.Android/AndroidGameActivity.cs b/osu.Framework.Android/AndroidGameActivity.cs index b4e9b95224..6fe92dbca2 100644 --- a/osu.Framework.Android/AndroidGameActivity.cs +++ b/osu.Framework.Android/AndroidGameActivity.cs @@ -17,10 +17,12 @@ public abstract class AndroidGameActivity : Activity { protected const ConfigChanges DEFAULT_CONFIG_CHANGES = ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden + | ConfigChanges.Navigation | ConfigChanges.Orientation | ConfigChanges.ScreenLayout | ConfigChanges.ScreenSize | ConfigChanges.SmallestScreenSize + | ConfigChanges.Touchscreen | ConfigChanges.UiMode; protected const LaunchMode DEFAULT_LAUNCH_MODE = LaunchMode.SingleInstance; diff --git a/osu.Framework.Android/AndroidGameHost.cs b/osu.Framework.Android/AndroidGameHost.cs index dd4e434f29..08df06dc04 100644 --- a/osu.Framework.Android/AndroidGameHost.cs +++ b/osu.Framework.Android/AndroidGameHost.cs @@ -51,6 +51,7 @@ protected override void SetupConfig(IDictionary defaul protected override IEnumerable CreateAvailableInputHandlers() => new InputHandler[] { + new AndroidMouseHandler(gameView), new AndroidKeyboardHandler(gameView), new AndroidTouchHandler(gameView), new MidiHandler() diff --git a/osu.Framework.Android/AndroidGameView.cs b/osu.Framework.Android/AndroidGameView.cs index 43caa8f6ee..18375134b0 100644 --- a/osu.Framework.Android/AndroidGameView.cs +++ b/osu.Framework.Android/AndroidGameView.cs @@ -19,12 +19,6 @@ public class AndroidGameView : osuTK.Android.AndroidGameView private readonly Game game; - public new event Action KeyDown; - public new event Action KeyUp; - public event Action KeyLongPress; - public event Action CommitText; - public event Action HostStarted; - public AndroidGameView(Context context, Game game) : base(context) { @@ -141,5 +135,34 @@ public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) outAttrs.InputType = InputTypes.TextVariationVisiblePassword | InputTypes.TextFlagNoSuggestions; return new AndroidInputConnection(this, true); } + + #region Events + + /// + /// Invoked on a key down event. + /// + public new event Action KeyDown; + + /// + /// Invoked on a key up event. + /// + public new event Action KeyUp; + + /// + /// Invoked on a key long press event. + /// + public event Action KeyLongPress; + + /// + /// Invoked when text is committed by an . + /// + public event Action CommitText; + + /// + /// Invoked when the has been started on the . + /// + public event Action HostStarted; + + #endregion } } diff --git a/osu.Framework.Android/Input/AndroidInputExtensions.cs b/osu.Framework.Android/Input/AndroidInputExtensions.cs new file mode 100644 index 0000000000..19c6a31f63 --- /dev/null +++ b/osu.Framework.Android/Input/AndroidInputExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Android.Views; +using osuTK.Input; + +namespace osu.Framework.Android.Input +{ + public static class AndroidInputExtensions + { + /// + /// Returns the corresponding for a mouse button given as a . + /// + /// The given button. Must not be a raw state or a non-mouse button. + /// The corresponding . + /// Thrown if the provided button is not a + public static MouseButton ToMouseButton(this MotionEventButtonState motionEventMouseButton) + { + switch (motionEventMouseButton) + { + case MotionEventButtonState.Primary: + return MouseButton.Left; + + case MotionEventButtonState.Secondary: + return MouseButton.Right; + + case MotionEventButtonState.Tertiary: + return MouseButton.Middle; + + case MotionEventButtonState.Back: + return MouseButton.Button1; + + case MotionEventButtonState.Forward: + return MouseButton.Button2; + + default: + throw new ArgumentOutOfRangeException(nameof(motionEventMouseButton), motionEventMouseButton, "Given button is not a mouse button."); + } + } + + /// + /// Returns the corresponding for a mouse button given as a . + /// + /// The given keycode. Should be or . + /// The corresponding . + /// true if this is a valid . + public static bool TryGetMouseButton(this Keycode keycode, out MouseButton button) + { + switch (keycode) + { + case Keycode.Back: + button = MouseButton.Button1; + return true; + + case Keycode.Forward: + button = MouseButton.Button2; + return true; + } + + button = MouseButton.LastButton; + return false; + } + } +} diff --git a/osu.Framework.Android/Input/AndroidInputHandler.cs b/osu.Framework.Android/Input/AndroidInputHandler.cs new file mode 100644 index 0000000000..d23ac72195 --- /dev/null +++ b/osu.Framework.Android/Input/AndroidInputHandler.cs @@ -0,0 +1,209 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Android.Views; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Input.Handlers; +using osu.Framework.Platform; + +#nullable enable + +namespace osu.Framework.Android.Input +{ + /// + /// Base input handler for handling events dispatched by . + /// Provides consistent and unified means for handling s only from specific s. + /// + public abstract class AndroidInputHandler : InputHandler + { + /// + /// The s that this will handle. + /// + protected abstract IEnumerable HandledEventSources { get; } + + /// + /// The view that this is handling events from. + /// + protected readonly AndroidGameView View; + + /// + /// Bitmask of all . + /// + private InputSourceType eventSourceBitmask; + + protected AndroidInputHandler(AndroidGameView view) + { + View = view; + } + + public override bool Initialize(GameHost host) + { + if (!base.Initialize(host)) + return false; + + // compute the bitmask for later use. + foreach (var eventSource in HandledEventSources) + { + // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags + // (InputSourceType is a flags enum, but is not properly marked as such) + eventSourceBitmask |= eventSource; + } + + return true; + } + + /// + /// Invoked on every event that matches an from . + /// + /// + /// Subscribe to . to receive events here. + /// + protected virtual void OnCapturedPointer(MotionEvent capturedPointerEvent) + { + throw new NotSupportedException($"{nameof(HandleCapturedPointer)} subscribed to {nameof(View.CapturedPointer)} but the relevant method was not overriden."); + } + + /// + /// Invoked on every event that matches an from . + /// + /// + /// Subscribe to . to receive events here. + /// + protected virtual void OnGenericMotion(MotionEvent genericMotionEvent) + { + throw new NotSupportedException($"{nameof(HandleGenericMotion)} subscribed to {nameof(View.GenericMotion)} but the relevant method was not overriden."); + } + + /// + /// Invoked on every event that matches an from . + /// + /// + /// Subscribe to . to receive events here. + /// + protected virtual void OnHover(MotionEvent hoverEvent) + { + throw new NotSupportedException($"{nameof(HandleHover)} subscribed to {nameof(View.Hover)} but the relevant method was not overriden."); + } + + /// + /// Invoked on every event that matches an from . + /// + /// + /// Subscribe to . to receive events here. + /// + protected virtual void OnKeyDown(Keycode keycode, KeyEvent e) + { + throw new NotSupportedException($"{nameof(HandleKeyDown)} subscribed to {nameof(View.KeyDown)} but the relevant method was not overriden."); + } + + /// + /// Invoked on every event that matches an from . + /// + /// + /// Subscribe to . to receive events here. + /// + protected virtual void OnKeyUp(Keycode keycode, KeyEvent e) + { + throw new NotSupportedException($"{nameof(HandleKeyUp)} subscribed to {nameof(View.KeyUp)} but the relevant method was not overriden."); + } + + /// + /// Invoked on every event that matches an from . + /// + /// + /// Subscribe to . to receive events here. + /// + protected virtual void OnTouch(MotionEvent touchEvent) + { + throw new NotSupportedException($"{nameof(HandleTouch)} subscribed to {nameof(View.Touch)} but the relevant method was not overriden."); + } + + #region Event handlers + + /// + /// Checks whether the should be handled by this . + /// + /// The to check. + /// true if the 's matches . + /// Should be checked before handling events. + private bool shouldHandleEvent([NotNullWhen(true)] InputEvent? inputEvent) + { + return inputEvent != null && eventSourceBitmask.HasFlagFast(inputEvent.Source); + } + + /// + /// Handler for events. + /// + protected void HandleCapturedPointer(object sender, View.CapturedPointerEventArgs e) + { + if (shouldHandleEvent(e.Event)) + { + OnCapturedPointer(e.Event); + e.Handled = true; + } + } + + /// + /// Handler for events. + /// + protected void HandleGenericMotion(object sender, View.GenericMotionEventArgs e) + { + if (shouldHandleEvent(e.Event)) + { + OnGenericMotion(e.Event); + e.Handled = true; + } + } + + /// + /// Handler for events. + /// + protected void HandleHover(object sender, View.HoverEventArgs e) + { + if (shouldHandleEvent(e.Event)) + { + OnHover(e.Event); + e.Handled = true; + } + } + + /// + /// Handler for events. + /// + protected void HandleKeyDown(Keycode keycode, KeyEvent e) + { + if (shouldHandleEvent(e)) + { + OnKeyDown(keycode, e); + } + } + + /// + /// Handler for events. + /// + protected void HandleKeyUp(Keycode keycode, KeyEvent e) + { + if (shouldHandleEvent(e)) + { + OnKeyUp(keycode, e); + } + } + + /// + /// Handler for events. + /// + protected void HandleTouch(object sender, View.TouchEventArgs e) + { + if (shouldHandleEvent(e.Event)) + { + OnTouch(e.Event); + e.Handled = true; + } + } + + #endregion + } +} diff --git a/osu.Framework.Android/Input/AndroidKeyboardHandler.cs b/osu.Framework.Android/Input/AndroidKeyboardHandler.cs index ef6a117e4d..d7bc3c5b40 100644 --- a/osu.Framework.Android/Input/AndroidKeyboardHandler.cs +++ b/osu.Framework.Android/Input/AndroidKeyboardHandler.cs @@ -2,30 +2,48 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using Android.Views; -using osu.Framework.Input.Handlers; using osu.Framework.Input.StateChanges; using osu.Framework.Platform; using osuTK.Input; namespace osu.Framework.Android.Input { - public class AndroidKeyboardHandler : InputHandler + public class AndroidKeyboardHandler : AndroidInputHandler { - private readonly AndroidGameView view; + protected override IEnumerable HandledEventSources => new[] { InputSourceType.Keyboard }; public AndroidKeyboardHandler(AndroidGameView view) + : base(view) { - this.view = view; - view.KeyDown += keyDown; - view.KeyUp += keyUp; } - public override bool IsActive => true; + public override bool Initialize(GameHost host) + { + if (!base.Initialize(host)) + return false; - public override bool Initialize(GameHost host) => true; + Enabled.BindValueChanged(enabled => + { + if (enabled.NewValue) + { + View.KeyDown += HandleKeyDown; + View.KeyUp += HandleKeyUp; + } + else + { + View.KeyDown -= HandleKeyDown; + View.KeyUp -= HandleKeyUp; + } + }, true); + + return true; + } - private void keyDown(Keycode keycode, KeyEvent e) + public override bool IsActive => true; + + protected override void OnKeyDown(Keycode keycode, KeyEvent e) { var key = GetKeyCodeAsKey(keycode); @@ -33,7 +51,7 @@ private void keyDown(Keycode keycode, KeyEvent e) PendingInputs.Enqueue(new KeyboardKeyInput(key, true)); } - private void keyUp(Keycode keycode, KeyEvent e) + protected override void OnKeyUp(Keycode keycode, KeyEvent e) { var key = GetKeyCodeAsKey(keycode); @@ -157,12 +175,5 @@ public static Key GetKeyCodeAsKey(Keycode keyCode) // this is the worst case scenario. Please note that the osu-framework keyboard handling cannot cope with Key.Unknown. return Key.Unknown; } - - protected override void Dispose(bool disposing) - { - view.KeyDown -= keyDown; - view.KeyUp -= keyUp; - base.Dispose(disposing); - } } } diff --git a/osu.Framework.Android/Input/AndroidMouseHandler.cs b/osu.Framework.Android/Input/AndroidMouseHandler.cs new file mode 100644 index 0000000000..e2c3a553f9 --- /dev/null +++ b/osu.Framework.Android/Input/AndroidMouseHandler.cs @@ -0,0 +1,134 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using Android.Views; +using osu.Framework.Input.StateChanges; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osuTK; +using osuTK.Input; + +namespace osu.Framework.Android.Input +{ + /// + /// Input handler for Android mouse-type devices: the and . + /// + public class AndroidMouseHandler : AndroidInputHandler + { + public override string Description => "Mouse"; + + public override bool IsActive => true; + + protected override IEnumerable HandledEventSources => new[] { InputSourceType.Mouse, InputSourceType.Touchpad }; + + public AndroidMouseHandler(AndroidGameView view) + : base(view) + { + } + + public override bool Initialize(GameHost host) + { + if (!base.Initialize(host)) + return false; + + Enabled.BindValueChanged(enabled => + { + if (enabled.NewValue) + { + View.GenericMotion += HandleGenericMotion; + View.Hover += HandleHover; + View.KeyDown += HandleKeyDown; + View.KeyUp += HandleKeyUp; + View.Touch += HandleTouch; + } + else + { + View.GenericMotion -= HandleGenericMotion; + View.Hover -= HandleHover; + View.KeyDown -= HandleKeyDown; + View.KeyUp -= HandleKeyUp; + View.Touch -= HandleTouch; + } + }, true); + + return true; + } + + protected override void OnKeyDown(Keycode keycode, KeyEvent e) + { + // some implementations might send Mouse1 and Mouse2 as keyboard keycodes, so we handle those here. + if (keycode.TryGetMouseButton(out var button)) + handleMouseDown(button); + } + + protected override void OnKeyUp(Keycode keycode, KeyEvent e) + { + if (keycode.TryGetMouseButton(out var button)) + handleMouseUp(button); + } + + protected override void OnHover(MotionEvent hoverEvent) + { + switch (hoverEvent.Action) + { + case MotionEventActions.HoverMove: + handleMouseMoveEvent(hoverEvent); + break; + } + } + + protected override void OnTouch(MotionEvent touchEvent) + { + switch (touchEvent.Action) + { + case MotionEventActions.Move: + handleMouseMoveEvent(touchEvent); + break; + } + } + + protected override void OnGenericMotion(MotionEvent genericMotionEvent) + { + switch (genericMotionEvent.Action) + { + case MotionEventActions.ButtonPress: + handleMouseDown(genericMotionEvent.ActionButton.ToMouseButton()); + break; + + case MotionEventActions.ButtonRelease: + handleMouseUp(genericMotionEvent.ActionButton.ToMouseButton()); + break; + + case MotionEventActions.Scroll: + handleMouseWheel(getEventScroll(genericMotionEvent)); + break; + } + } + + private Vector2 getEventScroll(MotionEvent e) => new Vector2(e.GetAxisValue(Axis.Hscroll), e.GetAxisValue(Axis.Vscroll)); + + private void handleMouseMoveEvent(MotionEvent evt) + { + // https://developer.android.com/reference/android/View/MotionEvent#batching + for (int i = 0; i < evt.HistorySize; i++) + handleMouseMove(new Vector2(evt.GetHistoricalX(i), evt.GetHistoricalY(i))); + + handleMouseMove(new Vector2(evt.RawX, evt.RawY)); + } + + private void handleMouseMove(Vector2 position) => enqueueInput(new MousePositionAbsoluteInput { Position = position }); + + private void handleMouseDown(MouseButton button) => enqueueInput(new MouseButtonInput(button, true)); + + private void handleMouseUp(MouseButton button) => enqueueInput(new MouseButtonInput(button, false)); + + private void handleMouseWheel(Vector2 delta) => enqueueInput(new MouseScrollRelativeInput { Delta = delta }); + + private void enqueueInput(IInput input) + { + PendingInputs.Enqueue(input); + FrameStatistics.Increment(StatisticsCounterType.MouseEvents); + } + } +} diff --git a/osu.Framework.Android/Input/AndroidTouchHandler.cs b/osu.Framework.Android/Input/AndroidTouchHandler.cs index 31d0f38860..8f838076a7 100644 --- a/osu.Framework.Android/Input/AndroidTouchHandler.cs +++ b/osu.Framework.Android/Input/AndroidTouchHandler.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using Android.Views; using osu.Framework.Input; -using osu.Framework.Input.Handlers; using osu.Framework.Input.StateChanges; using osu.Framework.Input.States; using osu.Framework.Platform; @@ -13,36 +13,54 @@ namespace osu.Framework.Android.Input { - public class AndroidTouchHandler : InputHandler + public class AndroidTouchHandler : AndroidInputHandler { - private readonly AndroidGameView view; - public override bool IsActive => true; + protected override IEnumerable HandledEventSources => new[] { InputSourceType.BluetoothStylus, InputSourceType.Stylus, InputSourceType.Touchscreen }; + public AndroidTouchHandler(AndroidGameView view) + : base(view) { - this.view = view; - view.Touch += handleTouch; - view.Hover += handleHover; } - public override bool Initialize(GameHost host) => true; + public override bool Initialize(GameHost host) + { + if (!base.Initialize(host)) + return false; + + Enabled.BindValueChanged(enabled => + { + if (enabled.NewValue) + { + View.Hover += HandleHover; + View.Touch += HandleTouch; + } + else + { + View.Hover -= HandleHover; + View.Touch -= HandleTouch; + } + }, true); + + return true; + } - private void handleTouch(object sender, View.TouchEventArgs e) + protected override void OnTouch(MotionEvent touchEvent) { - if (e.Event.Action == MotionEventActions.Move) + if (touchEvent.Action == MotionEventActions.Move) { - for (int i = 0; i < Math.Min(e.Event.PointerCount, TouchState.MAX_TOUCH_COUNT); i++) + for (int i = 0; i < Math.Min(touchEvent.PointerCount, TouchState.MAX_TOUCH_COUNT); i++) { - var touch = getEventTouch(e.Event, i); + var touch = getEventTouch(touchEvent, i); PendingInputs.Enqueue(new TouchInput(touch, true)); } } - else if (e.Event.ActionIndex < TouchState.MAX_TOUCH_COUNT) + else if (touchEvent.ActionIndex < TouchState.MAX_TOUCH_COUNT) { - var touch = getEventTouch(e.Event, e.Event.ActionIndex); + var touch = getEventTouch(touchEvent, touchEvent.ActionIndex); - switch (e.Event.ActionMasked) + switch (touchEvent.ActionMasked) { case MotionEventActions.Down: case MotionEventActions.PointerDown: @@ -58,20 +76,13 @@ private void handleTouch(object sender, View.TouchEventArgs e) } } - private void handleHover(object sender, View.HoverEventArgs e) + protected override void OnHover(MotionEvent hoverEvent) { - PendingInputs.Enqueue(new MousePositionAbsoluteInput { Position = getEventPosition(e.Event) }); - PendingInputs.Enqueue(new MouseButtonInput(MouseButton.Right, e.Event.ButtonState == MotionEventButtonState.StylusPrimary)); + PendingInputs.Enqueue(new MousePositionAbsoluteInput { Position = getEventPosition(hoverEvent) }); + PendingInputs.Enqueue(new MouseButtonInput(MouseButton.Right, hoverEvent.IsButtonPressed(MotionEventButtonState.StylusPrimary))); } private Touch getEventTouch(MotionEvent e, int index) => new Touch((TouchSource)e.GetPointerId(index), getEventPosition(e, index)); - private Vector2 getEventPosition(MotionEvent e, int index = 0) => new Vector2(e.GetX(index) * view.ScaleX, e.GetY(index) * view.ScaleY); - - protected override void Dispose(bool disposing) - { - view.Touch -= handleTouch; - view.Hover -= handleHover; - base.Dispose(disposing); - } + private Vector2 getEventPosition(MotionEvent e, int index = 0) => new Vector2(e.GetX(index) * View.ScaleX, e.GetY(index) * View.ScaleY); } } diff --git a/osu.Framework/Properties/AssemblyInfo.cs b/osu.Framework/Properties/AssemblyInfo.cs index 1a546239be..d8a61dffd8 100644 --- a/osu.Framework/Properties/AssemblyInfo.cs +++ b/osu.Framework/Properties/AssemblyInfo.cs @@ -7,6 +7,7 @@ // Note, that we omit visual tests as they are meant to test the framework // behavior "in the wild". +[assembly: InternalsVisibleTo("osu.Framework.Android")] [assembly: InternalsVisibleTo("osu.Framework.Benchmarks")] [assembly: InternalsVisibleTo("osu.Framework.Tests")] [assembly: InternalsVisibleTo("osu.Framework.Tests.Dynamic")]