Unverified Commit f545f47d authored by Amir Hardon's avatar Amir Hardon Committed by GitHub

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.
parent d3b16ecf
...@@ -50,7 +50,36 @@ typedef PlatformViewCreatedCallback = void Function(int id); ...@@ -50,7 +50,36 @@ typedef PlatformViewCreatedCallback = void Function(int id);
/// ///
/// See also: [PlatformView]. /// See also: [PlatformView].
class PlatformViewsService { class PlatformViewsService {
PlatformViewsService._(); PlatformViewsService._() {
SystemChannels.platform_views.setMethodCallHandler(_onMethodCall);
}
static PlatformViewsService _serviceInstance;
static PlatformViewsService get _instance {
_serviceInstance ??= PlatformViewsService._();
return _serviceInstance;
}
Future<void> _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<int, VoidCallback> _focusCallbacks = <int, VoidCallback>{};
/// Creates a controller for a new Android view. /// Creates a controller for a new Android view.
/// ///
...@@ -68,6 +97,9 @@ class PlatformViewsService { ...@@ -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-). /// 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]. /// 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 /// The Android view will only be created after [AndroidViewController.setSize] is called for the
/// first time. /// first time.
/// ///
...@@ -79,18 +111,21 @@ class PlatformViewsService { ...@@ -79,18 +111,21 @@ class PlatformViewsService {
@required TextDirection layoutDirection, @required TextDirection layoutDirection,
dynamic creationParams, dynamic creationParams,
MessageCodec<dynamic> creationParamsCodec, MessageCodec<dynamic> creationParamsCodec,
VoidCallback onFocus,
}) { }) {
assert(id != null); assert(id != null);
assert(viewType != null); assert(viewType != null);
assert(layoutDirection != null); assert(layoutDirection != null);
assert(creationParams == null || creationParamsCodec != null); assert(creationParams == null || creationParamsCodec != null);
return AndroidViewController._( final AndroidViewController controller = AndroidViewController._(
id, id,
viewType, viewType,
creationParams, creationParams,
creationParamsCodec, creationParamsCodec,
layoutDirection, layoutDirection,
); );
_instance._focusCallbacks[id] = onFocus ?? () {};
return controller;
} }
// TODO(amirh): reference the iOS plugin API for registering a UIView factory once it lands. // TODO(amirh): reference the iOS plugin API for registering a UIView factory once it lands.
......
...@@ -9,6 +9,8 @@ import 'package:flutter/services.dart'; ...@@ -9,6 +9,8 @@ import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'debug.dart'; import 'debug.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
/// Embeds an Android view in the Widget hierarchy. /// Embeds an Android view in the Widget hierarchy.
...@@ -295,16 +297,20 @@ class _AndroidViewState extends State<AndroidView> { ...@@ -295,16 +297,20 @@ class _AndroidViewState extends State<AndroidView> {
AndroidViewController _controller; AndroidViewController _controller;
TextDirection _layoutDirection; TextDirection _layoutDirection;
bool _initialized = false; bool _initialized = false;
FocusNode _focusNode;
static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet = static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
<Factory<OneSequenceGestureRecognizer>>{}; <Factory<OneSequenceGestureRecognizer>>{};
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _AndroidPlatformView( return Focus(
focusNode: _focusNode,
child: _AndroidPlatformView(
controller: _controller, controller: _controller,
hitTestBehavior: widget.hitTestBehavior, hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet, gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
),
); );
} }
...@@ -314,6 +320,7 @@ class _AndroidViewState extends State<AndroidView> { ...@@ -314,6 +320,7 @@ class _AndroidViewState extends State<AndroidView> {
} }
_initialized = true; _initialized = true;
_createNewAndroidView(); _createNewAndroidView();
_focusNode = FocusNode(debugLabel: 'AndroidView(id: $_id)');
} }
@override @override
...@@ -369,6 +376,9 @@ class _AndroidViewState extends State<AndroidView> { ...@@ -369,6 +376,9 @@ class _AndroidViewState extends State<AndroidView> {
layoutDirection: _layoutDirection, layoutDirection: _layoutDirection,
creationParams: widget.creationParams, creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec, creationParamsCodec: widget.creationParamsCodec,
onFocus: () {
_focusNode.requestFocus();
}
); );
if (widget.onPlatformViewCreated != null) { if (widget.onPlatformViewCreated != null) {
_controller.addOnPlatformViewCreatedListener(widget.onPlatformViewCreated); _controller.addOnPlatformViewCreatedListener(widget.onPlatformViewCreated);
......
...@@ -32,6 +32,12 @@ class FakeAndroidPlatformViewsController { ...@@ -32,6 +32,12 @@ class FakeAndroidPlatformViewsController {
_registeredViewTypes.add(viewType); _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<dynamic> _onMethodCall(MethodCall call) { Future<dynamic> _onMethodCall(MethodCall call) {
switch(call.method) { switch(call.method) {
case 'create': case 'create':
......
...@@ -853,6 +853,58 @@ void main() { ...@@ -853,6 +853,58 @@ void main() {
handle.dispose(); 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<void>();
final GlobalKey containerKey = GlobalKey();
await tester.pumpWidget(
Center(
child: Column(
children: <Widget>[
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', () { group('UiKitView', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment