diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index 78726aedc2..89f47dfa54 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -59,16 +59,21 @@ class PlatformViewsService { /// /// The Android view will only be created after [AndroidViewController.setSize] is called for the /// first time. + /// + /// The `id, `viewType, and `layoutDirection` parameters must not be null. static AndroidViewController initAndroidView({ @required int id, @required String viewType, + @required TextDirection layoutDirection, PlatformViewCreatedCallback onPlatformViewCreated, }) { assert(id != null); assert(viewType != null); + assert(layoutDirection != null); return new AndroidViewController._( id, viewType, + layoutDirection, onPlatformViewCreated ); } @@ -343,10 +348,13 @@ class AndroidViewController { AndroidViewController._( this.id, String viewType, + TextDirection layoutDirection, PlatformViewCreatedCallback onPlatformViewCreated, ) : assert(id != null), assert(viewType != null), + assert(layoutDirection != null), _viewType = viewType, + _layoutDirection = layoutDirection, _onPlatformViewCreated = onPlatformViewCreated, _state = _AndroidViewState.waitingForSize; @@ -380,6 +388,12 @@ class AndroidViewController { /// Android's [MotionEvent.ACTION_POINTER_UP](https://developer.android.com/reference/android/view/MotionEvent#ACTION_POINTER_UP) static const int kActionPointerUp = 6; + /// Android's [View.LAYOUT_DIRECTION_LTR](https://developer.android.com/reference/android/view/View.html#LAYOUT_DIRECTION_LTR) value. + static const int kAndroidLayoutDirectionLtr = 0; + + /// Android's [View.LAYOUT_DIRECTION_RTL](https://developer.android.com/reference/android/view/View.html#LAYOUT_DIRECTION_RTL) value. + static const int kAndroidLayoutDirectionRtl = 1; + /// The unique identifier of the Android view controlled by this controller. final int id; @@ -396,6 +410,8 @@ class AndroidViewController { /// disposed. int get textureId => _textureId; + TextDirection _layoutDirection; + _AndroidViewState _state; /// Disposes the Android view. @@ -430,6 +446,37 @@ class AndroidViewController { }); } + /// Sets the layout direction for the Android view. + Future setLayoutDirection(TextDirection layoutDirection) async { + if (_state == _AndroidViewState.disposed) + throw new FlutterError('trying to set a layout direction for a disposed Android View. View id: $id'); + + if (layoutDirection == _layoutDirection) + return; + + assert(layoutDirection != null); + _layoutDirection = layoutDirection; + + // If the view was not yet created we just update _layoutDirection and return, as the new + // direction will be used in _create. + if (_state == _AndroidViewState.waitingForSize) + return; + + await SystemChannels.platform_views.invokeMethod('setDirection', { + 'id': id, + 'direction': _getAndroidDirection(layoutDirection), + }); + } + + static int _getAndroidDirection(TextDirection direction) { + switch (direction) { + case TextDirection.ltr: + return kAndroidLayoutDirectionLtr; + case TextDirection.rtl: + return kAndroidLayoutDirectionRtl; + } + } + /// Sends an Android [MotionEvent](https://developer.android.com/reference/android/view/MotionEvent) /// to the view. /// @@ -454,6 +501,7 @@ class AndroidViewController { 'viewType': _viewType, 'width': size.width, 'height': size.height, + 'direction': _getAndroidDirection(_layoutDirection), }); if (_onPlatformViewCreated != null) _onPlatformViewCreated(id); diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index 7d8f467016..9e5bf1861e 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'basic.dart'; +import 'debug.dart'; import 'framework.dart'; /// Embeds an Android view in the Widget hierarchy. @@ -45,6 +47,7 @@ class AndroidView extends StatefulWidget { @required this.viewType, this.onPlatformViewCreated, this.hitTestBehavior = PlatformViewHitTestBehavior.opaque, + this.layoutDirection, }) : assert(viewType != null), assert(hitTestBehavior != null), super(key: key); @@ -66,6 +69,11 @@ class AndroidView extends StatefulWidget { /// This defaults to [PlatformViewHitTestBehavior.opaque]. final PlatformViewHitTestBehavior hitTestBehavior; + /// The text direction to use for the embedded view. + /// + /// If this is null, the ambient [Directionality] is used instead. + final TextDirection layoutDirection; + @override State createState() => new _AndroidViewState(); } @@ -73,6 +81,8 @@ class AndroidView extends StatefulWidget { class _AndroidViewState extends State { int _id; AndroidViewController _controller; + TextDirection _layoutDirection; + bool _initialized = false; @override Widget build(BuildContext context) { @@ -82,19 +92,53 @@ class _AndroidViewState extends State { ); } - @override - void initState() { - super.initState(); + void _initializeOnce() { + if (_initialized) { + return; + } + _initialized = true; + _layoutDirection = _findLayoutDirection(); _createNewAndroidView(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _initializeOnce(); + + final TextDirection newLayoutDirection = _findLayoutDirection(); + final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection; + _layoutDirection = newLayoutDirection; + + if (didChangeLayoutDirection) { + // The native view will update asynchronously, in the meantime we don't want + // to block the framework. (so this is intentionally not awaiting). + _controller.setLayoutDirection(_layoutDirection); + } + } + @override void didUpdateWidget(AndroidView oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.viewType == oldWidget.viewType) + + final TextDirection newLayoutDirection = _findLayoutDirection(); + final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection; + _layoutDirection = newLayoutDirection; + + if (widget.viewType != oldWidget.viewType) { + _controller.dispose(); + _createNewAndroidView(); return; - _controller.dispose(); - _createNewAndroidView(); + } + + if (didChangeLayoutDirection) { + _controller.setLayoutDirection(_layoutDirection); + } + } + + TextDirection _findLayoutDirection() { + assert(widget.layoutDirection != null || debugCheckHasDirectionality(context)); + return widget.layoutDirection ?? Directionality.of(context); } @override @@ -108,10 +152,10 @@ class _AndroidViewState extends State { _controller = PlatformViewsService.initAndroidView( id: _id, viewType: widget.viewType, + layoutDirection: _layoutDirection, onPlatformViewCreated: widget.onPlatformViewCreated ); } - } class _AndroidPlatformView extends LeafRenderObjectWidget { diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index 50d4baefb5..76f792c378 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -47,6 +47,8 @@ class FakePlatformViewsController { return _resize(call); case 'touch': return _touch(call); + case 'setDirection': + return _setDirection(call); } return new Future.sync(() => null); } @@ -57,6 +59,7 @@ class FakePlatformViewsController { final String viewType = args['viewType']; final double width = args['width']; final double height = args['height']; + final int layoutDirection = args['direction']; if (_views.containsKey(id)) throw new PlatformException( @@ -70,7 +73,7 @@ class FakePlatformViewsController { message: 'Trying to create a platform view of unregistered type: $viewType', ); - _views[id] = new FakePlatformView(id, viewType, new Size(width, height)); + _views[id] = new FakePlatformView(id, viewType, new Size(width, height), layoutDirection); final int textureId = _textureCounter++; return new Future.sync(() => textureId); } @@ -130,15 +133,31 @@ class FakePlatformViewsController { return new Future.sync(() => null); } + Future _setDirection(MethodCall call) async { + final Map args = call.arguments; + final int id = args['id']; + final int layoutDirection = args['direction']; + + if (!_views.containsKey(id)) + throw new PlatformException( + code: 'error', + message: 'Trying to resize a platform view with unknown id: $id', + ); + + _views[id].layoutDirection = layoutDirection; + + return new Future.sync(() => null); + } } class FakePlatformView { - FakePlatformView(this.id, this.type, this.size); + FakePlatformView(this.id, this.type, this.size, this.layoutDirection); final int id; final String type; Size size; + int layoutDirection; @override bool operator ==(dynamic other) { @@ -151,11 +170,11 @@ class FakePlatformView { } @override - int get hashCode => hashValues(id, type, size); + int get hashCode => hashValues(id, type, size, layoutDirection); @override String toString() { - return 'FakePlatformView(id: $id, type: $type, size: $size)'; + return 'FakePlatformView(id: $id, type: $type, size: $size, layoutDirection: $layoutDirection)'; } } diff --git a/packages/flutter/test/services/platform_views_test.dart b/packages/flutter/test/services/platform_views_test.dart index 09e2b5a3c9..d8866e5279 100644 --- a/packages/flutter/test/services/platform_views_test.dart +++ b/packages/flutter/test/services/platform_views_test.dart @@ -19,57 +19,66 @@ void main() { test('create Android view of unregistered type', () async { expect( - () => PlatformViewsService.initAndroidView( - id: 0, viewType: 'web').setSize(const Size(100.0, 100.0)), - throwsA(isInstanceOf())); + () { + return PlatformViewsService.initAndroidView( + id: 0, + viewType: 'web', + layoutDirection: TextDirection.ltr, + ).setSize(const Size(100.0, 100.0)); + }, + throwsA(isInstanceOf()), + ); }); test('create Android views', () async { viewsController.registerViewType('webview'); - await PlatformViewsService.initAndroidView( - id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0)); - await PlatformViewsService.initAndroidView( - id: 1, viewType: 'webview').setSize(const Size(200.0, 300.0)); + await PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr) + .setSize(const Size(100.0, 100.0)); + await PlatformViewsService.initAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl) + .setSize(const Size(200.0, 300.0)); expect( viewsController.views, unorderedEquals([ - new FakePlatformView(0, 'webview', const Size(100.0, 100.0)), - new FakePlatformView(1, 'webview', const Size(200.0, 300.0)), + new FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + new FakePlatformView(1, 'webview', const Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl), ])); }); test('reuse Android view id', () async { viewsController.registerViewType('webview'); await PlatformViewsService.initAndroidView( - id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0)); + id: 0, + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ).setSize(const Size(100.0, 100.0)); expect( () => PlatformViewsService.initAndroidView( - id: 0, viewType: 'web').setSize(const Size(100.0, 100.0)), + id: 0, viewType: 'web', layoutDirection: TextDirection.ltr).setSize(const Size(100.0, 100.0)), throwsA(isInstanceOf())); }); test('dispose Android view', () async { viewsController.registerViewType('webview'); await PlatformViewsService.initAndroidView( - id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0)); + id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr).setSize(const Size(100.0, 100.0)); final AndroidViewController viewController = - PlatformViewsService.initAndroidView(id: 1, viewType: 'webview'); + PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr); await viewController.setSize(const Size(200.0, 300.0)); viewController.dispose(); expect( viewsController.views, unorderedEquals([ - new FakePlatformView(0, 'webview', const Size(100.0, 100.0)), + new FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), ])); }); test('dispose inexisting Android view', () async { viewsController.registerViewType('webview'); await PlatformViewsService.initAndroidView( - id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0)); + id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr).setSize(const Size(100.0, 100.0)); final AndroidViewController viewController = - PlatformViewsService.initAndroidView(id: 1, viewType: 'webview'); + PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr); await viewController.setSize(const Size(200.0, 300.0)); await viewController.dispose(); await viewController.dispose(); @@ -78,16 +87,16 @@ void main() { test('resize Android view', () async { viewsController.registerViewType('webview'); await PlatformViewsService.initAndroidView( - id: 0, viewType: 'webview').setSize(const Size(100.0, 100.0)); + id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr).setSize(const Size(100.0, 100.0)); final AndroidViewController viewController = - PlatformViewsService.initAndroidView(id: 1, viewType: 'webview'); + PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr); await viewController.setSize(const Size(200.0, 300.0)); await viewController.setSize(const Size(500.0, 500.0)); expect( viewsController.views, unorderedEquals([ - new FakePlatformView(0, 'webview', const Size(100.0, 100.0)), - new FakePlatformView(1, 'webview', const Size(500.0, 500.0)), + new FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + new FakePlatformView(1, 'webview', const Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr), ])); }); @@ -97,19 +106,45 @@ void main() { final PlatformViewCreatedCallback callback = (int id) { createdViews.add(id); }; final AndroidViewController controller1 = PlatformViewsService.initAndroidView( - id: 0, viewType: 'webview', onPlatformViewCreated: callback); + id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr, onPlatformViewCreated: callback); expect(createdViews, isEmpty); await controller1.setSize(const Size(100.0, 100.0)); expect(createdViews, orderedEquals([0])); final AndroidViewController controller2 = PlatformViewsService.initAndroidView( - id: 5, viewType: 'webview', onPlatformViewCreated: callback); + id: 5, viewType: 'webview', layoutDirection: TextDirection.ltr, onPlatformViewCreated: callback); expect(createdViews, orderedEquals([0])); await controller2.setSize(const Size(100.0, 200.0)); expect(createdViews, orderedEquals([0, 5])); }); + + test('change Android view\'s directionality before creation', () async { + viewsController.registerViewType('webview'); + final AndroidViewController viewController = + PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.rtl); + await viewController.setLayoutDirection(TextDirection.ltr); + await viewController.setSize(const Size(100.0, 100.0)); + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + ])); + }); + + test('change Android view\'s directionality after creation', () async { + viewsController.registerViewType('webview'); + final AndroidViewController viewController = + PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + await viewController.setSize(const Size(100.0, 100.0)); + await viewController.setLayoutDirection(TextDirection.rtl); + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl), + ])); + }); }); } diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index fa03c02df5..fb4f9e3f92 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -24,7 +24,7 @@ void main() { child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'webview'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), ); @@ -32,7 +32,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0)) + new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) ]), ); }); @@ -46,7 +46,7 @@ void main() { child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'webview'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), ); @@ -58,7 +58,7 @@ void main() { child: SizedBox( width: 100.0, height: 50.0, - child: AndroidView(viewType: 'webview'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), ); @@ -70,7 +70,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0)) + new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) ]), ); @@ -80,7 +80,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - new FakePlatformView(currentViewId + 1, 'webview', const Size(100.0, 50.0)) + new FakePlatformView(currentViewId + 1, 'webview', const Size(100.0, 50.0), AndroidViewController.kAndroidLayoutDirectionLtr) ]), ); }); @@ -95,7 +95,7 @@ void main() { child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'webview'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), ); @@ -105,7 +105,7 @@ void main() { child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'maps'), + child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.ltr), ), ), ); @@ -113,7 +113,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - new FakePlatformView(currentViewId + 2, 'maps', const Size(200.0, 100.0)) + new FakePlatformView(currentViewId + 2, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) ]), ); }); @@ -126,7 +126,7 @@ void main() { child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'webview'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), ); @@ -156,7 +156,7 @@ void main() { child: new SizedBox( width: 200.0, height: 100.0, - child: new AndroidView(viewType: 'webview', key: key), + child: new AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), ), ), ); @@ -167,7 +167,7 @@ void main() { child: new SizedBox( width: 200.0, height: 100.0, - child: new AndroidView(viewType: 'webview', key: key), + child: new AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), ), ), ), @@ -176,7 +176,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0)) + new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) ]), ); }); @@ -191,7 +191,7 @@ void main() { child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'webview'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr,), ), ), ); @@ -230,6 +230,7 @@ void main() { child: AndroidView( viewType: 'webview', hitTestBehavior: PlatformViewHitTestBehavior.transparent, + layoutDirection: TextDirection.ltr, ), ), ), @@ -272,6 +273,7 @@ void main() { child: AndroidView( viewType: 'webview', hitTestBehavior: PlatformViewHitTestBehavior.translucent, + layoutDirection: TextDirection.ltr, ), ), ), @@ -316,6 +318,7 @@ void main() { child: AndroidView( viewType: 'webview', hitTestBehavior: PlatformViewHitTestBehavior.opaque, + layoutDirection: TextDirection.ltr, ), ), ), @@ -350,7 +353,7 @@ void main() { child: const SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'webview'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), ), @@ -367,4 +370,88 @@ void main() { ]), ); }); + + testWidgets('Android view directionality', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.rtl), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl) + ]), + ); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); + + testWidgets('Android view ambient directionality', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.rtl, + child: Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps'), + ), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl) + ]), + ); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps'), + ), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + new FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); }