-
Notifications
You must be signed in to change notification settings - Fork 18
Android support #1
Changes from 7 commits
b659efe
e1e3f44
16e3aa6
ebe2332
1b35cf7
6b55c34
7fe762e
741d583
8492d96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package com.example.flutter_gamepad | ||
|
||
import android.view.KeyEvent | ||
|
||
/** | ||
* The buttons on a gamepad, in flutter_gamepad order. This order matches the one on the Dart side. | ||
*/ | ||
enum class Button { | ||
A, | ||
B, | ||
X, | ||
Y, | ||
DpadUp, | ||
DpadDown, | ||
DpadLeft, | ||
DpadRight, | ||
Menu, | ||
Options, | ||
LeftThumbstickButton, | ||
RightThumbstickButton, | ||
LeftShoulder, | ||
RightShoulder, | ||
LeftTrigger, | ||
RightTrigger, | ||
} | ||
|
||
/** | ||
* The thumbsticks on a gamepad, in flutter_gamepad order. This order matches the one on the Dart side. | ||
*/ | ||
enum class Thumbstick { | ||
Left, | ||
Right, | ||
} | ||
|
||
/** | ||
* A map from Android key-codes to the flutter_gamepad Button enum. | ||
*/ | ||
val buttonMap = hashMapOf( | ||
KeyEvent.KEYCODE_BUTTON_A to Button.A, | ||
KeyEvent.KEYCODE_BUTTON_B to Button.B, | ||
KeyEvent.KEYCODE_BUTTON_X to Button.X, | ||
KeyEvent.KEYCODE_BUTTON_Y to Button.Y, | ||
KeyEvent.KEYCODE_DPAD_UP to Button.DpadUp, | ||
KeyEvent.KEYCODE_DPAD_DOWN to Button.DpadDown, | ||
KeyEvent.KEYCODE_DPAD_LEFT to Button.DpadLeft, | ||
KeyEvent.KEYCODE_DPAD_RIGHT to Button.DpadRight, | ||
KeyEvent.KEYCODE_BUTTON_START to Button.Menu, | ||
KeyEvent.KEYCODE_BUTTON_SELECT to Button.Options, | ||
KeyEvent.KEYCODE_BUTTON_THUMBL to Button.LeftThumbstickButton, | ||
KeyEvent.KEYCODE_BUTTON_THUMBR to Button.RightThumbstickButton, | ||
KeyEvent.KEYCODE_BUTTON_L1 to Button.LeftShoulder, | ||
KeyEvent.KEYCODE_BUTTON_R1 to Button.RightShoulder, | ||
KeyEvent.KEYCODE_BUTTON_L2 to Button.LeftTrigger, | ||
KeyEvent.KEYCODE_BUTTON_R2 to Button.RightTrigger | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,93 @@ | ||
package com.example.flutter_gamepad | ||
|
||
import android.view.KeyEvent | ||
import android.view.MotionEvent | ||
import io.flutter.embedding.android.AndroidKeyProcessor | ||
import io.flutter.embedding.android.AndroidTouchProcessor | ||
import io.flutter.view.FlutterView | ||
import io.flutter.embedding.engine.renderer.FlutterRenderer | ||
import io.flutter.embedding.engine.systemchannels.KeyEventChannel | ||
import io.flutter.plugin.common.EventChannel | ||
import io.flutter.plugin.common.MethodCall | ||
import io.flutter.plugin.common.MethodChannel | ||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler | ||
import io.flutter.plugin.common.MethodChannel.Result | ||
import io.flutter.plugin.common.PluginRegistry.Registrar | ||
import io.flutter.plugin.editing.TextInputPlugin | ||
import java.lang.reflect.Field | ||
|
||
class FlutterGamepadPlugin: MethodCallHandler { | ||
companion object { | ||
@JvmStatic | ||
fun registerWith(registrar: Registrar) { | ||
val channel = MethodChannel(registrar.messenger(), "flutter_gamepad") | ||
channel.setMethodCallHandler(FlutterGamepadPlugin()) | ||
/** | ||
* An extension of Flutter's AndroidTouchProcessor that delegates MotionEvents to the GamepadStreamHandler. | ||
* (On Android, thumbstick events from a gamepad are a kind of MotionEvent.) | ||
*/ | ||
class GamepadAndroidTouchProcessor(renderer: FlutterRenderer) : AndroidTouchProcessor(renderer) { | ||
override fun onGenericMotionEvent(event: MotionEvent): Boolean { | ||
return GamepadStreamHandler.processMotionEvent(event) || super.onGenericMotionEvent(event) | ||
} | ||
} | ||
|
||
/** | ||
* An extension of Flutter's AndroidKeyProcessor that delegates KeyEvents to the GamepadStreamHandler. | ||
* (On Android, button events from a gamepad are a kind of KeyEvent.) | ||
*/ | ||
class GamepadAndroidKeyProcessor(keyEventChannel: KeyEventChannel, textInputPlugin: TextInputPlugin) : AndroidKeyProcessor(keyEventChannel, textInputPlugin) { | ||
override fun onKeyDown(keyEvent: KeyEvent) { | ||
val handled = GamepadStreamHandler.processKeyDownEvent(keyEvent) | ||
if (!handled) { | ||
super.onKeyDown(keyEvent) | ||
} | ||
} | ||
|
||
override fun onKeyUp(keyEvent: KeyEvent) { | ||
val handled = GamepadStreamHandler.processKeyUpEvent(keyEvent) | ||
if (!handled) { | ||
super.onKeyDown(keyEvent) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* The flutter_gamepad plugin class that is registered with the framework. | ||
*/ | ||
class FlutterGamepadPlugin : MethodCallHandler { | ||
companion object { | ||
@JvmStatic | ||
fun registerWith(registrar: Registrar) { | ||
val methodChannel = MethodChannel(registrar.messenger(), "com.rainway.flutter_gamepad/methods") | ||
methodChannel.setMethodCallHandler(FlutterGamepadPlugin()) | ||
val eventChannel = EventChannel(registrar.messenger(), "com.rainway.flutter_gamepad/events") | ||
eventChannel.setStreamHandler(GamepadStreamHandler) | ||
|
||
val view: FlutterView = registrar.view() | ||
fun viewField(name: String): Field { | ||
val field = FlutterView::class.java.getDeclaredField(name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need a try/catch? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See below: if this process goes wrong for someone after a Flutter-internal change, we can't really save anything and probably want to let that error propagate so they can tell us it broke. |
||
field.isAccessible = true | ||
return field | ||
} | ||
|
||
// Hack: swap in a new AndroidTouchProcessor. | ||
val touchProcessorField = viewField("androidTouchProcessor") | ||
val rendererField = viewField("flutterRenderer") | ||
val renderer = rendererField.get(view) as FlutterRenderer | ||
val touchProcessor = GamepadAndroidTouchProcessor(renderer) | ||
touchProcessorField.set(view, touchProcessor) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "Hack" comment seems to suggest that this solution is tenuous in that it could possibly break if Flutter changes something about how it is implemented. For example if the "androidTouchProcessor" field changes names perhaps? If something like that were to happen would this code throw an uncaught exception? If so, should we try to catch the exception here and deal with it gracefully? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes — it's relying on the presence of a (private!) field with that name in a class in the Flutter engine, which could change in a future version. This patching step is crucial to the working of the plugin: if it goes wrong, then the plug-in can't possibly do its job (because we will have no access to key/motion events). In a sense, crashing feels like the “right” thing to do, then: users of the plug-in will make an issue here on GitHub, and it will be up to us to fix and accommodate the new Flutter version. If we preempt such a failure and It would be a different story if the Flutter version wasn't “baked into” an app. That is: if the trick works (for the user) in a given build of an app, then it won't suddenly stop working. So users of this plug-in (i.e. Flutter app developers) don't have to worry about this hack suddenly breaking on them if it works once for them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this what RawKeyboard is for in combination with InputDevice using a https://api.flutter.dev/flutter/services/RawKeyEventDataAndroid-class.html There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you (as a Flutter app developer) can use a RawKeyboard Flutter widget to get gamepad button events. However, at the time of writing, that works only on Android, and only buttons are supported: the motion events from thumbsticks are discarded by AndroidTouchProcessor. (The events also don't seem to have a If we want to offer a plug-in that has the same API on both iOS and Android, i.e. if we want to put button events onto the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated to this PR, but just had the thought that it might be a good idea to specify on this https://pub.dev/packages/flutter_gamepad that it is tested and works with specific versions of Flutter and that it is potentially not compatible with versions that haven't been tested. Also I'm curious, if the flutter_gamepad plugin did cause a crash due to incompatibility with the version of Flutter that the Flutter app developer is using it with, would it be obvious to the developer why/where it crashed (assuming the developer has no background in platform specific development so doing something like running the app through XCode is too foreign for them to try)? In other words, if it were to crash after said app developer did a "flutter run" on their app, would they be able to tell that it is the gamepad_plugin that is malfunctioning? |
||
|
||
// Hack: swap in a new AndroidKeyProcessor. | ||
val keyProcessorField = viewField("androidKeyProcessor") | ||
val keyEventChannelField = viewField("keyEventChannel") | ||
val textInputPluginField = viewField("mTextInputPlugin") | ||
val keyEventChannel = keyEventChannelField.get(view) as KeyEventChannel | ||
val textInputPlugin = textInputPluginField.get(view) as TextInputPlugin | ||
val keyProcessor = GamepadAndroidKeyProcessor(keyEventChannel, textInputPlugin) | ||
keyProcessorField.set(view, keyProcessor) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment here regarding "Hack". |
||
} | ||
} | ||
} | ||
|
||
override fun onMethodCall(call: MethodCall, result: Result) { | ||
if (call.method == "getPlatformVersion") { | ||
result.success("Android ${android.os.Build.VERSION.RELEASE}") | ||
} else { | ||
result.notImplemented() | ||
override fun onMethodCall(call: MethodCall, result: Result) { | ||
if (call.method == "gamepads") { | ||
result.success(allGamepadInfoDictionaries()) | ||
} else { | ||
result.notImplemented() | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious, but why/how do gamepads trigger key events?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, on Android a gamepad button press is a kind of KeyEvent. :)
(And a gamepad thumbstick movement is a kind of MotionEvent, just like a mouse or pointer movement.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added comments explaining this