Skip to content

Commit f852092

Browse files
authored
Add Focus support for iOS platform view (#103019)
1 parent 5c135cc commit f852092

File tree

4 files changed

+95
-7
lines changed

4 files changed

+95
-7
lines changed

packages/flutter/lib/src/services/platform_views.dart

+6
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ class PlatformViewsService {
199199
/// factory for this view type must have been registered on the platform side.
200200
/// Platform view factories are typically registered by plugin code.
201201
///
202+
/// `onFocus` is a callback that will be invoked when the UIKit view asks to
203+
/// get the input focus.
202204
/// The `id, `viewType, and `layoutDirection` parameters must not be null.
203205
/// If `creationParams` is non null then `creationParamsCodec` must not be null.
204206
static Future<UiKitViewController> initUiKitView({
@@ -207,6 +209,7 @@ class PlatformViewsService {
207209
required TextDirection layoutDirection,
208210
dynamic creationParams,
209211
MessageCodec<dynamic>? creationParamsCodec,
212+
VoidCallback? onFocus,
210213
}) async {
211214
assert(id != null);
212215
assert(viewType != null);
@@ -227,6 +230,9 @@ class PlatformViewsService {
227230
);
228231
}
229232
await SystemChannels.platform_views.invokeMethod<void>('create', args);
233+
if (onFocus != null) {
234+
_instance._focusCallbacks[id] = onFocus;
235+
}
230236
return UiKitViewController._(id, layoutDirection);
231237
}
232238
}

packages/flutter/lib/src/widgets/platform_view.dart

+20-5
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ class _UiKitViewState extends State<UiKitView> {
562562
UiKitViewController? _controller;
563563
TextDirection? _layoutDirection;
564564
bool _initialized = false;
565+
late FocusNode _focusNode;
565566

566567
static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
567568
<Factory<OneSequenceGestureRecognizer>>{};
@@ -571,10 +572,14 @@ class _UiKitViewState extends State<UiKitView> {
571572
if (_controller == null) {
572573
return const SizedBox.expand();
573574
}
574-
return _UiKitPlatformView(
575-
controller: _controller!,
576-
hitTestBehavior: widget.hitTestBehavior,
577-
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
575+
return Focus(
576+
focusNode: _focusNode,
577+
onFocusChange: _onFocusChange,
578+
child: _UiKitPlatformView(
579+
controller: _controller!,
580+
hitTestBehavior: widget.hitTestBehavior,
581+
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
582+
),
578583
);
579584
}
580585

@@ -639,13 +644,23 @@ class _UiKitViewState extends State<UiKitView> {
639644
layoutDirection: _layoutDirection!,
640645
creationParams: widget.creationParams,
641646
creationParamsCodec: widget.creationParamsCodec,
647+
onFocus: () {
648+
_focusNode.requestFocus();
649+
}
642650
);
643651
if (!mounted) {
644652
controller.dispose();
645653
return;
646654
}
647655
widget.onPlatformViewCreated?.call(id);
648-
setState(() { _controller = controller; });
656+
setState(() {
657+
_controller = controller;
658+
_focusNode = FocusNode(debugLabel: 'UiKitView(id: $id)');
659+
});
660+
}
661+
662+
void _onFocusChange(bool isFocused) {
663+
// TODO(hellohuanlin): send 'TextInput.setPlatformViewClient' channel message to engine after the engine is updated to handle this message.
649664
}
650665
}
651666

packages/flutter/test/services/fake_platform_views.dart

+7
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,13 @@ class FakeIosPlatformViewsController {
340340
_registeredViewTypes.add(viewType);
341341
}
342342

343+
void invokeViewFocused(int viewId) {
344+
final MethodCodec codec = SystemChannels.platform_views.codec;
345+
final ByteData data = codec.encodeMethodCall(MethodCall('viewFocused', viewId));
346+
ServicesBinding.instance.defaultBinaryMessenger
347+
.handlePlatformMessage(SystemChannels.platform_views.name, data, (ByteData? data) {});
348+
}
349+
343350
Future<dynamic> _onMethodCall(MethodCall call) {
344351
switch(call.method) {
345352
case 'create':

packages/flutter/test/widgets/platform_view_test.dart

+62-2
Original file line numberDiff line numberDiff line change
@@ -1978,7 +1978,7 @@ void main() {
19781978
},
19791979
);
19801980

1981-
testWidgets('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async {
1981+
testWidgets('UiKitView rebuilt with same gestureRecognizers', (WidgetTester tester) async {
19821982
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
19831983
viewsController.registerViewType('webview');
19841984

@@ -2012,6 +2012,59 @@ void main() {
20122012
expect(factoryInvocationCount, 1);
20132013
});
20142014

2015+
testWidgets('UiKitView can take input focus', (WidgetTester tester) async {
2016+
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
2017+
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
2018+
viewsController.registerViewType('webview');
2019+
2020+
final GlobalKey containerKey = GlobalKey();
2021+
await tester.pumpWidget(
2022+
Center(
2023+
child: Column(
2024+
children: <Widget>[
2025+
const SizedBox(
2026+
width: 200.0,
2027+
height: 100.0,
2028+
child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
2029+
),
2030+
Focus(
2031+
debugLabel: 'container',
2032+
child: Container(key: containerKey),
2033+
),
2034+
],
2035+
),
2036+
),
2037+
);
2038+
2039+
// First frame is before the platform view was created so the render object
2040+
// is not yet in the tree.
2041+
await tester.pump();
2042+
2043+
final Focus uiKitViewFocusWidget = tester.widget(
2044+
find.descendant(
2045+
of: find.byType(UiKitView),
2046+
matching: find.byType(Focus),
2047+
),
2048+
);
2049+
final FocusNode uiKitViewFocusNode = uiKitViewFocusWidget.focusNode!;
2050+
final Element containerElement = tester.element(find.byKey(containerKey));
2051+
final FocusNode containerFocusNode = Focus.of(containerElement);
2052+
2053+
containerFocusNode.requestFocus();
2054+
2055+
await tester.pump();
2056+
2057+
expect(containerFocusNode.hasFocus, isTrue);
2058+
expect(uiKitViewFocusNode.hasFocus, isFalse);
2059+
2060+
viewsController.invokeViewFocused(currentViewId + 1);
2061+
2062+
await tester.pump();
2063+
2064+
expect(containerFocusNode.hasFocus, isFalse);
2065+
expect(uiKitViewFocusNode.hasFocus, isTrue);
2066+
});
2067+
20152068
testWidgets('UiKitView has correct semantics', (WidgetTester tester) async {
20162069
final SemanticsHandle handle = tester.ensureSemantics();
20172070
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
@@ -2039,7 +2092,14 @@ void main() {
20392092
// is not yet in the tree.
20402093
await tester.pump();
20412094

2042-
final SemanticsNode semantics = tester.getSemantics(find.byType(UiKitView));
2095+
final SemanticsNode semantics = tester.getSemantics(
2096+
find.descendant(
2097+
of: find.byType(UiKitView),
2098+
matching: find.byWidgetPredicate(
2099+
(Widget widget) => widget.runtimeType.toString() == '_UiKitPlatformView',
2100+
),
2101+
),
2102+
);
20432103

20442104
expect(semantics.platformViewId, currentViewId + 1);
20452105
expect(semantics.rect, const Rect.fromLTWH(0, 0, 200, 100));

0 commit comments

Comments
 (0)