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.
This commit is contained in:
Amir Hardon 2019-05-15 15:25:50 -07:00 committed by GitHub
parent d3b16ecfa0
commit f545f47d8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 3 deletions

View file

@ -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<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.
///
@ -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<dynamic> 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.

View file

@ -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<AndroidView> {
AndroidViewController _controller;
TextDirection _layoutDirection;
bool _initialized = false;
FocusNode _focusNode;
static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
<Factory<OneSequenceGestureRecognizer>>{};
@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<AndroidView> {
}
_initialized = true;
_createNewAndroidView();
_focusNode = FocusNode(debugLabel: 'AndroidView(id: $_id)');
}
@override
@ -369,6 +376,9 @@ class _AndroidViewState extends State<AndroidView> {
layoutDirection: _layoutDirection,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
onFocus: () {
_focusNode.requestFocus();
}
);
if (widget.onPlatformViewCreated != null) {
_controller.addOnPlatformViewCreatedListener(widget.onPlatformViewCreated);

View file

@ -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<dynamic> _onMethodCall(MethodCall call) {
switch(call.method) {
case 'create':

View file

@ -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<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', () {