From f85209272dca619d15cb3cbc59fc6681319f7da7 Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Mon, 23 May 2022 17:58:13 -0700 Subject: [PATCH] Add Focus support for iOS platform view (#103019) --- .../lib/src/services/platform_views.dart | 6 ++ .../lib/src/widgets/platform_view.dart | 25 ++++++-- .../test/services/fake_platform_views.dart | 7 ++ .../test/widgets/platform_view_test.dart | 64 ++++++++++++++++++- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index ee323c31cb3d..26c9dd10097c 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -199,6 +199,8 @@ class PlatformViewsService { /// factory for this view type must have been registered on the platform side. /// Platform view factories are typically registered by plugin code. /// + /// `onFocus` is a callback that will be invoked when the UIKit view asks to + /// get the input focus. /// The `id, `viewType, and `layoutDirection` parameters must not be null. /// If `creationParams` is non null then `creationParamsCodec` must not be null. static Future initUiKitView({ @@ -207,6 +209,7 @@ class PlatformViewsService { required TextDirection layoutDirection, dynamic creationParams, MessageCodec? creationParamsCodec, + VoidCallback? onFocus, }) async { assert(id != null); assert(viewType != null); @@ -227,6 +230,9 @@ class PlatformViewsService { ); } await SystemChannels.platform_views.invokeMethod('create', args); + if (onFocus != null) { + _instance._focusCallbacks[id] = onFocus; + } return UiKitViewController._(id, layoutDirection); } } diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index 5cbb0798df00..7f8d8c852693 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -562,6 +562,7 @@ class _UiKitViewState extends State { UiKitViewController? _controller; TextDirection? _layoutDirection; bool _initialized = false; + late FocusNode _focusNode; static final Set> _emptyRecognizersSet = >{}; @@ -571,10 +572,14 @@ class _UiKitViewState extends State { if (_controller == null) { return const SizedBox.expand(); } - return _UiKitPlatformView( - controller: _controller!, - hitTestBehavior: widget.hitTestBehavior, - gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet, + return Focus( + focusNode: _focusNode, + onFocusChange: _onFocusChange, + child: _UiKitPlatformView( + controller: _controller!, + hitTestBehavior: widget.hitTestBehavior, + gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet, + ), ); } @@ -639,13 +644,23 @@ class _UiKitViewState extends State { layoutDirection: _layoutDirection!, creationParams: widget.creationParams, creationParamsCodec: widget.creationParamsCodec, + onFocus: () { + _focusNode.requestFocus(); + } ); if (!mounted) { controller.dispose(); return; } widget.onPlatformViewCreated?.call(id); - setState(() { _controller = controller; }); + setState(() { + _controller = controller; + _focusNode = FocusNode(debugLabel: 'UiKitView(id: $id)'); + }); + } + + void _onFocusChange(bool isFocused) { + // TODO(hellohuanlin): send 'TextInput.setPlatformViewClient' channel message to engine after the engine is updated to handle this message. } } diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index e2c2442cdd4c..933e7c76958d 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -340,6 +340,13 @@ class FakeIosPlatformViewsController { _registeredViewTypes.add(viewType); } + void invokeViewFocused(int viewId) { + final MethodCodec codec = SystemChannels.platform_views.codec; + final ByteData data = codec.encodeMethodCall(MethodCall('viewFocused', viewId)); + ServicesBinding.instance.defaultBinaryMessenger + .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 ecf8295bef1d..eacc3282eb36 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -1978,7 +1978,7 @@ void main() { }, ); - testWidgets('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async { + testWidgets('UiKitView rebuilt with same gestureRecognizers', (WidgetTester tester) async { final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -2012,6 +2012,59 @@ void main() { expect(factoryInvocationCount, 1); }); + testWidgets('UiKitView can take input focus', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + + final GlobalKey containerKey = GlobalKey(); + await tester.pumpWidget( + Center( + child: Column( + children: [ + const SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + Focus( + debugLabel: 'container', + child: Container(key: containerKey), + ), + ], + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final Focus uiKitViewFocusWidget = tester.widget( + find.descendant( + of: find.byType(UiKitView), + matching: find.byType(Focus), + ), + ); + final FocusNode uiKitViewFocusNode = uiKitViewFocusWidget.focusNode!; + final Element containerElement = tester.element(find.byKey(containerKey)); + final FocusNode containerFocusNode = Focus.of(containerElement); + + containerFocusNode.requestFocus(); + + await tester.pump(); + + expect(containerFocusNode.hasFocus, isTrue); + expect(uiKitViewFocusNode.hasFocus, isFalse); + + viewsController.invokeViewFocused(currentViewId + 1); + + await tester.pump(); + + expect(containerFocusNode.hasFocus, isFalse); + expect(uiKitViewFocusNode.hasFocus, isTrue); + }); + testWidgets('UiKitView has correct semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); @@ -2039,7 +2092,14 @@ void main() { // is not yet in the tree. await tester.pump(); - final SemanticsNode semantics = tester.getSemantics(find.byType(UiKitView)); + final SemanticsNode semantics = tester.getSemantics( + find.descendant( + of: find.byType(UiKitView), + matching: find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_UiKitPlatformView', + ), + ), + ); expect(semantics.platformViewId, currentViewId + 1); expect(semantics.rect, const Rect.fromLTWH(0, 0, 200, 100));