Skip to content

Commit

Permalink
Add Focus support for iOS platform view (#103019)
Browse files Browse the repository at this point in the history
  • Loading branch information
hellohuanlin committed May 24, 2022
1 parent 5c135cc commit f852092
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 7 deletions.
6 changes: 6 additions & 0 deletions packages/flutter/lib/src/services/platform_views.dart
Expand Up @@ -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<UiKitViewController> initUiKitView({
Expand All @@ -207,6 +209,7 @@ class PlatformViewsService {
required TextDirection layoutDirection,
dynamic creationParams,
MessageCodec<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) async {
assert(id != null);
assert(viewType != null);
Expand All @@ -227,6 +230,9 @@ class PlatformViewsService {
);
}
await SystemChannels.platform_views.invokeMethod<void>('create', args);
if (onFocus != null) {
_instance._focusCallbacks[id] = onFocus;
}
return UiKitViewController._(id, layoutDirection);
}
}
Expand Down
25 changes: 20 additions & 5 deletions packages/flutter/lib/src/widgets/platform_view.dart
Expand Up @@ -562,6 +562,7 @@ class _UiKitViewState extends State<UiKitView> {
UiKitViewController? _controller;
TextDirection? _layoutDirection;
bool _initialized = false;
late FocusNode _focusNode;

static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
<Factory<OneSequenceGestureRecognizer>>{};
Expand All @@ -571,10 +572,14 @@ class _UiKitViewState extends State<UiKitView> {
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,
),
);
}

Expand Down Expand Up @@ -639,13 +644,23 @@ class _UiKitViewState extends State<UiKitView> {
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.
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/flutter/test/services/fake_platform_views.dart
Expand Up @@ -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<dynamic> _onMethodCall(MethodCall call) {
switch(call.method) {
case 'create':
Expand Down
64 changes: 62 additions & 2 deletions packages/flutter/test/widgets/platform_view_test.dart
Expand Up @@ -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');

Expand Down Expand Up @@ -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: <Widget>[
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();
Expand Down Expand Up @@ -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));
Expand Down

0 comments on commit f852092

Please sign in to comment.