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].
|
/// 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 {
|
|||||||
/// 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 {
|
|||||||
@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';
|
|||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
_createNewAndroidView();
|
_createNewAndroidView();
|
||||||
|
_focusNode = FocusNode(debugLabel: 'AndroidView(id: $_id)');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -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 {
|
|||||||
_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() {
|
|||||||
|
|
||||||
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', () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user