mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
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:
parent
d3b16ecfa0
commit
f545f47d8f
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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', () {
|
||||
|
|
Loading…
Reference in a new issue