From f545f47d8f06d3551baddb2e7752c7a9170c1214 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 15 May 2019 15:25:50 -0700 Subject: [PATCH] Add a FocusNode for AndroidView widgets. (#32773) The PlatformViewsService listens for `viewFocused` calls on the platform_views system channel and fires a callback that focuses the focus node for the relevant AndroidView widget. --- .../lib/src/services/platform_views.dart | 39 +++++++++++++- .../lib/src/widgets/platform_view.dart | 12 ++++- .../test/services/fake_platform_views.dart | 6 +++ .../test/widgets/platform_view_test.dart | 52 +++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index bfcddea3990..a32d343aeec 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -50,7 +50,36 @@ typedef PlatformViewCreatedCallback = void Function(int id); /// /// See also: [PlatformView]. class PlatformViewsService { - PlatformViewsService._(); + PlatformViewsService._() { + SystemChannels.platform_views.setMethodCallHandler(_onMethodCall); + } + + static PlatformViewsService _serviceInstance; + + static PlatformViewsService get _instance { + _serviceInstance ??= PlatformViewsService._(); + return _serviceInstance; + } + + Future _onMethodCall(MethodCall call) { + switch(call.method) { + case 'viewFocused': + final int id = call.arguments; + if (_focusCallbacks.containsKey(id)) { + _focusCallbacks[id](); + } + break; + default: + throw UnimplementedError('${call.method} was invoked but isn\'t implemented by PlatformViewsService'); + } + return null; + } + + /// Maps platform view IDs to focus callbacks. + /// + /// The callbacks are invoked when the platform view asks to be focused. + final Map _focusCallbacks = {}; + /// Creates a controller for a new Android view. /// @@ -68,6 +97,9 @@ class PlatformViewsService { /// platform side. It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-). /// This is typically one of: [StandardMessageCodec], [JSONMessageCodec], [StringCodec], or [BinaryCodec]. /// + /// `onFocus` is a callback that will be invoked when the Android View asks to get the + /// input focus. + /// /// The Android view will only be created after [AndroidViewController.setSize] is called for the /// first time. /// @@ -79,18 +111,21 @@ class PlatformViewsService { @required TextDirection layoutDirection, dynamic creationParams, MessageCodec creationParamsCodec, + VoidCallback onFocus, }) { assert(id != null); assert(viewType != null); assert(layoutDirection != null); assert(creationParams == null || creationParamsCodec != null); - return AndroidViewController._( + final AndroidViewController controller = AndroidViewController._( id, viewType, creationParams, creationParamsCodec, layoutDirection, ); + _instance._focusCallbacks[id] = onFocus ?? () {}; + return controller; } // TODO(amirh): reference the iOS plugin API for registering a UIView factory once it lands. diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index 2b947ac1a68..94f77505c7c 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -9,6 +9,8 @@ import 'package:flutter/services.dart'; import 'basic.dart'; import 'debug.dart'; +import 'focus_manager.dart'; +import 'focus_scope.dart'; import 'framework.dart'; /// Embeds an Android view in the Widget hierarchy. @@ -295,16 +297,20 @@ class _AndroidViewState extends State { AndroidViewController _controller; TextDirection _layoutDirection; bool _initialized = false; + FocusNode _focusNode; static final Set> _emptyRecognizersSet = >{}; @override Widget build(BuildContext context) { - return _AndroidPlatformView( + return Focus( + focusNode: _focusNode, + child: _AndroidPlatformView( controller: _controller, hitTestBehavior: widget.hitTestBehavior, gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet, + ), ); } @@ -314,6 +320,7 @@ class _AndroidViewState extends State { } _initialized = true; _createNewAndroidView(); + _focusNode = FocusNode(debugLabel: 'AndroidView(id: $_id)'); } @override @@ -369,6 +376,9 @@ class _AndroidViewState extends State { layoutDirection: _layoutDirection, creationParams: widget.creationParams, creationParamsCodec: widget.creationParamsCodec, + onFocus: () { + _focusNode.requestFocus(); + } ); if (widget.onPlatformViewCreated != null) { _controller.addOnPlatformViewCreatedListener(widget.onPlatformViewCreated); diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index 5af17ae96a0..83f81bc64b5 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -32,6 +32,12 @@ class FakeAndroidPlatformViewsController { _registeredViewTypes.add(viewType); } + void invokeViewFocused(int viewId) { + final MethodCodec codec = SystemChannels.platform_views.codec; + final ByteData data = codec.encodeMethodCall(MethodCall('viewFocused', viewId)); + BinaryMessages.handlePlatformMessage(SystemChannels.platform_views.name, data, (ByteData data) {}); + } + Future _onMethodCall(MethodCall call) { switch(call.method) { case 'create': diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index e61443a6007..b78bbaea384 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -853,6 +853,58 @@ void main() { handle.dispose(); }); + + testWidgets('AndroidView can take input focus', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + + viewsController.createCompleter = Completer(); + + final GlobalKey containerKey = GlobalKey(); + await tester.pumpWidget( + Center( + child: Column( + children: [ + const SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + Focus( + debugLabel: 'container', + child: Container(key: containerKey), + ), + ], + ), + ), + ); + + final Focus androidViewFocusWidget = + tester.widget( + find.descendant( + of: find.byType(AndroidView), + matching: find.byType(Focus) + ) + ); + final Element containerElement = tester.element(find.byKey(containerKey)); + final FocusNode androidViewFocusNode = androidViewFocusWidget.focusNode; + final FocusNode containerFocusNode = Focus.of(containerElement); + + containerFocusNode.requestFocus(); + + await tester.pump(); + + expect(containerFocusNode.hasFocus, isTrue); + expect(androidViewFocusNode.hasFocus, isFalse); + + viewsController.invokeViewFocused(currentViewId + 1); + + await tester.pump(); + + expect(containerFocusNode.hasFocus, isFalse); + expect(androidViewFocusNode.hasFocus, isTrue); + }); }); group('UiKitView', () {