From c68758fab137f751d56faa1ced119b7a3561e37e Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 24 Jun 2020 21:23:02 -0700 Subject: [PATCH] Implement delayed key event synthesis support for Android (#59358) --- .../lib/src/services/raw_keyboard.dart | 66 +++++++++++++++++-- .../lib/src/widgets/focus_manager.dart | 7 +- .../test/services/raw_keyboard_test.dart | 43 ++++++++++++ 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart index cc552c74ff..834ec50deb 100644 --- a/packages/flutter/lib/src/services/raw_keyboard.dart +++ b/packages/flutter/lib/src/services/raw_keyboard.dart @@ -467,6 +467,13 @@ class RawKeyUpEvent extends RawKeyEvent { }) : super(data: data, character: character); } +/// A callback type used by [RawKeyboard.keyEventHandler] to send key events to +/// a handler that can determine if the key has been handled or not. +/// +/// The handler should return true if the key has been handled, and false if the +/// key was not handled. It must not return null. +typedef RawKeyEventHandler = bool Function(RawKeyEvent event); + /// An interface for listening to raw key events. /// /// Raw key events pass through as much information as possible from the @@ -477,6 +484,9 @@ class RawKeyUpEvent extends RawKeyEvent { /// buttons that are represented as keys. Typically used by games and other apps /// that use keyboards for purposes other than text entry. /// +/// These key events are typically only key events generated by a hardware +/// keyboard, and not those from software keyboards or input method editors. +/// /// See also: /// /// * [RawKeyDownEvent] and [RawKeyUpEvent], the classes used to describe @@ -494,20 +504,61 @@ class RawKeyboard { final List> _listeners = >[]; - /// Calls the listener every time the user presses or releases a key. + /// Register a listener that is called every time the user presses or releases + /// a hardware keyboard key. + /// + /// Since the listeners have no way to indicate what they did with the event, + /// listeners are assumed to not handle the key event. These events will also + /// be distributed to other listeners, and to the [keyEventHandler]. + /// + /// Most applications prefer to use the focus system (see [Focus] and + /// [FocusManager]) to receive key events to the focused control instead of + /// this kind of passive listener. /// /// Listeners can be removed with [removeListener]. void addListener(ValueChanged listener) { _listeners.add(listener); } - /// Stop calling the listener every time the user presses or releases a key. + /// Stop calling the given listener every time the user presses or releases a + /// hardware keyboard key. /// /// Listeners can be added with [addListener]. void removeListener(ValueChanged listener) { _listeners.remove(listener); } + /// A handler for hardware keyboard events that will stop propagation if the + /// handler returns true. + /// + /// Key events on the platform are given to Flutter to be handled by the + /// engine. If they are not handled, then the platform will continue to + /// distribute the keys (i.e. propagate them) to other (possibly non-Flutter) + /// components in the application. The return value from this handler tells + /// the platform to either stop propagation (by returning true: "event + /// handled"), or pass the event on to other controls (false: "event not + /// handled"). + /// + /// This handler is normally set by the [FocusManager] so that it can control + /// the key event propagation to focused widgets. + /// + /// Most applications can use the focus system (see [Focus] and + /// [FocusManager]) to receive key events. If you are not using the + /// [FocusManager] to manage focus, then to be able to stop propagation of the + /// event by indicating that the event was handled, set this attribute to a + /// [RawKeyEventHandler]. Otherwise, key events will be assumed to not have + /// been handled by Flutter, and will also be sent to other (possibly + /// non-Flutter) controls in the application. + /// + /// See also: + /// + /// * [Focus.onKey], a [Focus] callback attribute that will be given key + /// events distributed by the [FocusManager] based on the current primary + /// focus. + /// * [addListener], to add passive key event listeners that do not stop event + /// propagation. + RawKeyEventHandler keyEventHandler; + Future _handleKeyEvent(dynamic message) async { final RawKeyEvent event = RawKeyEvent.fromMessage(message as Map); if (event == null) { @@ -534,14 +585,19 @@ class RawKeyboard { // Make sure that the modifiers reflect reality, in case a modifier key was // pressed/released while the app didn't have focus. _synchronizeModifiers(event); - if (_listeners.isEmpty) { - return; - } + // Send the event to passive listeners. for (final ValueChanged listener in List>.from(_listeners)) { if (_listeners.contains(listener)) { listener(event); } } + + // Send the key event to the keyEventHandler, then send the appropriate + // response to the platform so that it can resolve the event's handling. + // Defaults to false if keyEventHandler is null. + final bool handled = keyEventHandler != null && keyEventHandler(event); + assert(handled != null, 'keyEventHandler returned null, which is not allowed'); + return { 'handled': handled }; } static final Map<_ModifierSidePair, Set> _modifierKeyMap = <_ModifierSidePair, Set>{ diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index 5c3ad4c93e..89b4d68be1 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -1416,7 +1416,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { /// from the [WidgetsBinding] singleton). FocusManager() { rootScope._manager = this; - RawKeyboard.instance.addListener(_handleRawKeyEvent); + RawKeyboard.instance.keyEventHandler = _handleRawKeyEvent; GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent); } @@ -1605,7 +1605,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { } } - void _handleRawKeyEvent(RawKeyEvent event) { + bool _handleRawKeyEvent(RawKeyEvent event) { // Update highlightMode first, since things responding to the keys might // look at the highlight mode, and it should be accurate. _lastInteractionWasTouch = false; @@ -1616,7 +1616,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { // onKey on the way up, and if one responds that they handled it, stop. if (_primaryFocus == null) { assert(_focusDebug('No primary focus for key event, ignored: $event')); - return; + return false; } bool handled = false; for (final FocusNode node in [_primaryFocus, ..._primaryFocus.ancestors]) { @@ -1629,6 +1629,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { if (!handled) { assert(_focusDebug('Key event not handled by anyone: $event.')); } + return handled; } /// The node that currently has the primary focus. diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart index e7e5c09558..1d8ec9327d 100644 --- a/packages/flutter/test/services/raw_keyboard_test.dart +++ b/packages/flutter/test/services/raw_keyboard_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; class _ModifierCheck { @@ -508,6 +509,48 @@ void main() { final RawKeyEventDataAndroid data = repeatCountEvent.data as RawKeyEventDataAndroid; expect(data.repeatCount, equals(42)); }); + testWidgets('Key events are responded to correctly.', (WidgetTester tester) async { + expect(RawKeyboard.instance.keysPressed, isEmpty); + // Generate the data for a regular key down event. + final Map data = KeyEventSimulator.getKeyData( + LogicalKeyboardKey.keyA, + platform: 'android', + isDown: true, + ); + Map message; + await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + SystemChannels.keyEvent.name, + SystemChannels.keyEvent.codec.encodeMessage(data), + (ByteData data) { + message = SystemChannels.keyEvent.codec.decodeMessage(data) as Map; + }, + ); + expect(message, equals({ 'handled': false })); + + // Set up a widget that will receive focused text events. + final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + await tester.pumpWidget( + Focus( + focusNode: focusNode, + onKey: (FocusNode node, RawKeyEvent event) { + return true; // handle all events. + }, + child: const SizedBox(), + ), + ); + focusNode.requestFocus(); + await tester.pump(); + + await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + SystemChannels.keyEvent.name, + SystemChannels.keyEvent.codec.encodeMessage(data), + (ByteData data) { + message = SystemChannels.keyEvent.codec.decodeMessage(data) as Map; + }, + ); + expect(message, equals({ 'handled': true })); + ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(SystemChannels.keyEvent.name, null); + }); }); group('RawKeyEventDataFuchsia', () { const Map modifierTests = {