Browse Source

RTL Layout (#4575)

* Support rtl layout

* ios layout options

* Support rtl layout

* Use View.LAYOUT_DIRECTION_LTR constant instead of 0

* move layout direction logic to RootPresenter

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* fix tests

* Fix tests
Hadi Mostafapour 5 years ago
parent
commit
d09d0108d1

+ 8
- 0
lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutOptions.java View File

@@ -4,8 +4,12 @@ import com.reactnativenavigation.parse.params.Colour;
4 4
 import com.reactnativenavigation.parse.params.NullColor;
5 5
 import com.reactnativenavigation.parse.params.NullNumber;
6 6
 import com.reactnativenavigation.parse.params.Number;
7
+import com.reactnativenavigation.parse.params.Text;
8
+import com.reactnativenavigation.parse.params.NullText;
7 9
 import com.reactnativenavigation.parse.parsers.ColorParser;
8 10
 import com.reactnativenavigation.parse.parsers.NumberParser;
11
+import com.reactnativenavigation.parse.parsers.TextParser;
12
+
9 13
 
10 14
 import org.json.JSONObject;
11 15
 
@@ -18,6 +22,7 @@ public class LayoutOptions {
18 22
         result.componentBackgroundColor = ColorParser.parse(json, "componentBackgroundColor");
19 23
         result.topMargin = NumberParser.parse(json, "topMargin");
20 24
         result.orientation = OrientationOptions.parse(json);
25
+        result.direction = TextParser.parse(json, "direction");
21 26
 
22 27
         return result;
23 28
     }
@@ -26,12 +31,14 @@ public class LayoutOptions {
26 31
     public Colour componentBackgroundColor = new NullColor();
27 32
     public Number topMargin = new NullNumber();
28 33
     public OrientationOptions orientation = new OrientationOptions();
34
+    public Text direction = new NullText();
29 35
 
30 36
     public void mergeWith(LayoutOptions other) {
31 37
         if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;
32 38
         if (other.componentBackgroundColor.hasValue()) componentBackgroundColor = other.componentBackgroundColor;
33 39
         if (other.topMargin.hasValue()) topMargin = other.topMargin;
34 40
         if (other.orientation.hasValue()) orientation = other.orientation;
41
+        if (other.direction.hasValue()) direction = other.direction;
35 42
 
36 43
     }
37 44
 
@@ -40,5 +47,6 @@ public class LayoutOptions {
40 47
         if (!componentBackgroundColor.hasValue()) componentBackgroundColor = defaultOptions.componentBackgroundColor;
41 48
         if (!topMargin.hasValue()) topMargin = defaultOptions.topMargin;
42 49
         if (!orientation.hasValue()) orientation = defaultOptions.orientation;
50
+        if (!direction.hasValue()) direction = defaultOptions.direction;
43 51
     }
44 52
 }

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java View File

@@ -67,7 +67,7 @@ public class NavigationModule extends ReactContextBaseJavaModule {
67 67
 		handle(() -> {
68 68
             navigator().setEventEmitter(eventEmitter);
69 69
             final ViewController viewController = newLayoutFactory().create(layoutTree);
70
-            navigator().setRoot(viewController, new NativeCommandListener(commandId, promise, eventEmitter, now));
70
+            navigator().setRoot(viewController, new NativeCommandListener(commandId, promise, eventEmitter, now), reactInstanceManager);
71 71
         });
72 72
 	}
73 73
 

+ 2
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonController.java View File

@@ -102,7 +102,8 @@ public class TitleBarButtonController extends ViewController<TitleBarReactButton
102 102
     }
103 103
 
104 104
     public void applyNavigationIcon(Toolbar toolbar) {
105
-        navigationIconResolver.resolve(button, icon -> {
105
+        Integer direction = getActivity().getWindow().getDecorView().getLayoutDirection();
106
+        navigationIconResolver.resolve(button, direction, icon -> {
106 107
             setIconColor(icon);
107 108
             toolbar.setNavigationOnClickListener(view -> onPressListener.onPress(button.id));
108 109
             toolbar.setNavigationIcon(icon);

+ 4
- 2
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolver.java View File

@@ -5,6 +5,7 @@ import android.graphics.drawable.Drawable;
5 5
 import android.support.annotation.NonNull;
6 6
 import android.support.v4.content.ContextCompat;
7 7
 import android.util.Log;
8
+import android.view.View;
8 9
 
9 10
 import com.reactnativenavigation.R;
10 11
 import com.reactnativenavigation.parse.params.Button;
@@ -23,7 +24,7 @@ public class NavigationIconResolver {
23 24
         this.imageLoader = imageLoader;
24 25
     }
25 26
 
26
-    public void resolve(Button button, Func1<Drawable> onSuccess) {
27
+    public void resolve(Button button, Integer direction, Func1<Drawable> onSuccess) {
27 28
         if (button.icon.hasValue()) {
28 29
             imageLoader.loadIcon(context, button.icon.get(), new ImageLoadingListenerAdapter() {
29 30
                 @Override
@@ -37,7 +38,8 @@ public class NavigationIconResolver {
37 38
                 }
38 39
             });
39 40
         } else if (Constants.BACK_BUTTON_ID.equals(button.id)) {
40
-            onSuccess.run(ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_black_24dp));
41
+            Boolean isRTL = direction == View.LAYOUT_DIRECTION_RTL;
42
+            onSuccess.run(ContextCompat.getDrawable(context, isRTL ? R.drawable.ic_arrow_back_black_rtl_24dp : R.drawable.ic_arrow_back_black_24dp));
41 43
         } else {
42 44
             Log.w("RNN", "Left button needs to have an icon");
43 45
         }

+ 4
- 2
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/Navigator.java View File

@@ -21,6 +21,8 @@ import com.reactnativenavigation.viewcontrollers.ViewController;
21 21
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
22 22
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
23 23
 
24
+import com.facebook.react.ReactInstanceManager;
25
+
24 26
 import java.util.Collection;
25 27
 import java.util.Collections;
26 28
 import java.util.List;
@@ -125,7 +127,7 @@ public class Navigator extends ParentController {
125 127
 
126 128
     }
127 129
 
128
-    public void setRoot(final ViewController viewController, CommandListener commandListener) {
130
+    public void setRoot(final ViewController viewController, CommandListener commandListener, ReactInstanceManager reactInstanceManager) {
129 131
         destroyRoot();
130 132
         final boolean removeSplashView = isRootNotCreated();
131 133
         if (isRootNotCreated()) getView();
@@ -136,7 +138,7 @@ public class Navigator extends ParentController {
136 138
                 if (removeSplashView) removePreviousContentView();
137 139
                 super.onSuccess(childId);
138 140
             }
139
-        });
141
+        }, reactInstanceManager);
140 142
     }
141 143
 
142 144
     private void removePreviousContentView() {

+ 18
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java View File

@@ -2,6 +2,7 @@ package com.reactnativenavigation.viewcontrollers.navigator;
2 2
 
3 3
 import android.content.Context;
4 4
 import android.widget.FrameLayout;
5
+import android.view.View;
5 6
 
6 7
 import com.reactnativenavigation.anim.NavigationAnimator;
7 8
 import com.reactnativenavigation.parse.Options;
@@ -9,6 +10,10 @@ import com.reactnativenavigation.utils.CommandListener;
9 10
 import com.reactnativenavigation.viewcontrollers.ViewController;
10 11
 import com.reactnativenavigation.views.element.ElementTransitionManager;
11 12
 
13
+import com.facebook.react.modules.i18nmanager.I18nUtil;
14
+import com.facebook.react.bridge.ReactApplicationContext;
15
+import com.facebook.react.ReactInstanceManager;
16
+
12 17
 public class RootPresenter {
13 18
     private NavigationAnimator animator;
14 19
     private FrameLayout rootLayout;
@@ -25,7 +30,8 @@ public class RootPresenter {
25 30
         this.animator = animator;
26 31
     }
27 32
 
28
-    void setRoot(ViewController root, Options defaultOptions, CommandListener listener) {
33
+    void setRoot(ViewController root, Options defaultOptions, CommandListener listener, ReactInstanceManager reactInstanceManager) {
34
+        setLayoutDirection(root, defaultOptions, (ReactApplicationContext) reactInstanceManager.getCurrentReactContext());
29 35
         rootLayout.addView(root.getView());
30 36
         Options options = root.resolveCurrentOptions(defaultOptions);
31 37
         root.setWaitForRender(options.animations.setRoot.waitForRender);
@@ -47,4 +53,15 @@ public class RootPresenter {
47 53
             listener.onSuccess(root.getId());
48 54
         }
49 55
     }
56
+
57
+    private void setLayoutDirection(ViewController root, Options defaultOptions, ReactApplicationContext reactContext) {
58
+        if (defaultOptions.layout.direction.hasValue()) {
59
+            I18nUtil i18nUtil = I18nUtil.getInstance();
60
+            Boolean isRtl = defaultOptions.layout.direction.get().equals("rtl");
61
+
62
+            root.getActivity().getWindow().getDecorView().setLayoutDirection(isRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
63
+            i18nUtil.allowRTL(reactContext, isRtl);
64
+            i18nUtil.forceRTL(reactContext, isRtl);
65
+        }
66
+    }
50 67
 }

+ 4
- 2
lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java View File

@@ -101,6 +101,8 @@ public class TitleBar extends Toolbar {
101 101
     }
102 102
 
103 103
     private void alignTextView(Alignment alignment, TextView view) {
104
+        Integer direction = view.getParent().getLayoutDirection();
105
+        Boolean isRTL = direction == View.LAYOUT_DIRECTION_RTL;
104 106
         int width = view.getResources().getDisplayMetrics().widthPixels;
105 107
         view.post(() -> {
106 108
             if (alignment == Alignment.Center) {
@@ -108,9 +110,9 @@ public class TitleBar extends Toolbar {
108 110
                 //noinspection IntegerDivisionInFloatingPointContext
109 111
                 view.setX((width - view.getWidth()) / 2);
110 112
             } else if (leftButtonController != null) {
111
-                view.setX(getContentInsetStartWithNavigation());
113
+                view.setX(isRTL ? (getWidth() - view.getWidth()) - getContentInsetStartWithNavigation() : getContentInsetStartWithNavigation());
112 114
             } else {
113
-                view.setX(UiUtils.dpToPx(getContext(), DEFAULT_LEFT_MARGIN));
115
+                view.setX(isRTL ? (getWidth() - view.getWidth()) - UiUtils.dpToPx(getContext(), DEFAULT_LEFT_MARGIN) : UiUtils.dpToPx(getContext(), DEFAULT_LEFT_MARGIN));
114 116
             }
115 117
         });
116 118
     }

+ 9
- 0
lib/android/app/src/main/res/drawable/ic_arrow_back_black_rtl_24dp.xml View File

@@ -0,0 +1,9 @@
1
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+    android:width="24dp"
3
+    android:height="24dp"
4
+    android:viewportHeight="24.0"
5
+    android:viewportWidth="24.0">
6
+    <path
7
+        android:fillColor="#000000"
8
+        android:pathData="M4,13H16.15l-5.6,5.6L12,20l8-8L12,4,10.6,5.4,16.15,11H4Z" />
9
+</vector>

+ 3
- 2
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolverTest.java View File

@@ -3,6 +3,7 @@ package com.reactnativenavigation.viewcontrollers.button;
3 3
 import android.content.Context;
4 4
 import android.graphics.Color;
5 5
 import android.graphics.drawable.Drawable;
6
+import android.view.View;
6 7
 
7 8
 import com.reactnativenavigation.BaseTest;
8 9
 import com.reactnativenavigation.mocks.ImageLoaderMock;
@@ -42,7 +43,7 @@ public class NavigationIconResolverTest extends BaseTest {
42 43
 
43 44
             }
44 45
         });
45
-        uut.resolve(iconButton(), onSuccess);
46
+        uut.resolve(iconButton(), View.LAYOUT_DIRECTION_LTR, onSuccess);
46 47
         verify(imageLoader).loadIcon(eq(context), eq(ICON_URI), any());
47 48
         verify(onSuccess).run(any(Drawable.class));
48 49
     }
@@ -55,7 +56,7 @@ public class NavigationIconResolverTest extends BaseTest {
55 56
 
56 57
             }
57 58
         });
58
-        uut.resolve(backButton(), onSuccess);
59
+        uut.resolve(backButton(), View.LAYOUT_DIRECTION_LTR, onSuccess);
59 60
         verifyZeroInteractions(imageLoader);
60 61
         verify(onSuccess).run(any());
61 62
     }

+ 38
- 34
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java View File

@@ -34,6 +34,8 @@ import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
34 34
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
35 35
 import com.reactnativenavigation.views.BottomTabs;
36 36
 
37
+import com.facebook.react.ReactInstanceManager;
38
+
37 39
 import org.junit.Test;
38 40
 import org.mockito.ArgumentCaptor;
39 41
 import org.mockito.Mockito;
@@ -71,11 +73,13 @@ public class NavigatorTest extends BaseTest {
71 73
     private EventEmitter eventEmitter;
72 74
     private ViewController.ViewVisibilityListener parentVisibilityListener;
73 75
     private ModalStack modalStack;
76
+    private ReactInstanceManager reactInstanceManager;
74 77
 
75 78
     @Override
76 79
     public void beforeEach() {
77 80
         childRegistry = new ChildControllersRegistry();
78 81
         eventEmitter = Mockito.mock(EventEmitter.class);
82
+        reactInstanceManager = Mockito.mock(ReactInstanceManager.class);
79 83
         overlayManager = spy(new OverlayManager());
80 84
         imageLoaderMock = ImageLoaderMock.mock();
81 85
         activityController = newActivityController(TestActivity.class);
@@ -122,7 +126,7 @@ public class NavigatorTest extends BaseTest {
122 126
         uut.setDefaultOptions(new Options());
123 127
 
124 128
         SimpleViewController spy = spy(child1);
125
-        uut.setRoot(spy, new CommandListenerAdapter());
129
+        uut.setRoot(spy, new CommandListenerAdapter(), reactInstanceManager);
126 130
         Options defaultOptions = new Options();
127 131
         uut.setDefaultOptions(defaultOptions);
128 132
 
@@ -133,9 +137,9 @@ public class NavigatorTest extends BaseTest {
133 137
     @Test
134 138
     public void setRoot_delegatesToRootPresenter() {
135 139
         CommandListenerAdapter listener = new CommandListenerAdapter();
136
-        uut.setRoot(child1, listener);
140
+        uut.setRoot(child1, listener, reactInstanceManager);
137 141
         ArgumentCaptor<CommandListenerAdapter> captor = ArgumentCaptor.forClass(CommandListenerAdapter.class);
138
-        verify(rootPresenter).setRoot(eq(child1), eq(uut.getDefaultOptions()), captor.capture());
142
+        verify(rootPresenter).setRoot(eq(child1), eq(uut.getDefaultOptions()), captor.capture(), eq(reactInstanceManager));
139 143
         assertThat(captor.getValue().getListener()).isEqualTo(listener);
140 144
     }
141 145
 
@@ -144,21 +148,21 @@ public class NavigatorTest extends BaseTest {
144 148
         FrameLayout content = activity.findViewById(android.R.id.content);
145 149
         assertThat(content.getChildCount()).isEqualTo(4); // 3 frame layouts and the default splash layout
146 150
 
147
-        uut.setRoot(child2, new CommandListenerAdapter());
151
+        uut.setRoot(child2, new CommandListenerAdapter(), reactInstanceManager);
148 152
 
149 153
         assertThat(content.getChildCount()).isEqualTo(3);
150 154
     }
151 155
 
152 156
     @Test
153 157
     public void setRoot_AddsChildControllerView() {
154
-        uut.setRoot(child1, new CommandListenerAdapter());
158
+        uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager);
155 159
         assertIsChild(uut.getRootLayout(), child1.getView());
156 160
     }
157 161
 
158 162
     @Test
159 163
     public void setRoot_ReplacesExistingChildControllerViews() {
160
-        uut.setRoot(child1, new CommandListenerAdapter());
161
-        uut.setRoot(child2, new CommandListenerAdapter());
164
+        uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager);
165
+        uut.setRoot(child2, new CommandListenerAdapter(), reactInstanceManager);
162 166
         assertIsChild(uut.getRootLayout(), child2.getView());
163 167
     }
164 168
 
@@ -172,7 +176,7 @@ public class NavigatorTest extends BaseTest {
172 176
     public void push() {
173 177
         StackController stackController = newStack();
174 178
         stackController.push(child1, new CommandListenerAdapter());
175
-        uut.setRoot(stackController, new CommandListenerAdapter());
179
+        uut.setRoot(stackController, new CommandListenerAdapter(), reactInstanceManager);
176 180
 
177 181
         assertIsChild(uut.getView(), stackController.getView());
178 182
         assertIsChild(stackController.getView(), child1.getView());
@@ -185,7 +189,7 @@ public class NavigatorTest extends BaseTest {
185 189
 
186 190
     @Test
187 191
     public void push_InvalidPushWithoutAStack_DoesNothing() {
188
-        uut.setRoot(child1, new CommandListenerAdapter());
192
+        uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager);
189 193
         uut.push(child1.getId(), child2, new CommandListenerAdapter());
190 194
         assertIsChild(uut.getView(), child1.getView());
191 195
     }
@@ -197,7 +201,7 @@ public class NavigatorTest extends BaseTest {
197 201
         stack1.push(child1, new CommandListenerAdapter());
198 202
         stack2.push(child2, new CommandListenerAdapter());
199 203
         BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2));
200
-        uut.setRoot(bottomTabsController, new CommandListenerAdapter());
204
+        uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager);
201 205
 
202 206
         SimpleViewController newChild = new SimpleViewController(activity, childRegistry, "new child", tabOptions);
203 207
         uut.push(child2.getId(), newChild, new CommandListenerAdapter());
@@ -216,7 +220,7 @@ public class NavigatorTest extends BaseTest {
216 220
     @Test
217 221
     public void pop_InvalidDoesNothing() {
218 222
         uut.pop("123", Options.EMPTY, new CommandListenerAdapter());
219
-        uut.setRoot(child1, new CommandListenerAdapter());
223
+        uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager);
220 224
         uut.pop(child1.getId(), Options.EMPTY, new CommandListenerAdapter());
221 225
         assertThat(uut.getChildControllers()).hasSize(1);
222 226
     }
@@ -226,7 +230,7 @@ public class NavigatorTest extends BaseTest {
226 230
         StackController stack1 = newStack();
227 231
         StackController stack2 = newStack();
228 232
         BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2));
229
-        uut.setRoot(bottomTabsController, new CommandListenerAdapter());
233
+        uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager);
230 234
         stack1.push(child1, new CommandListenerAdapter());
231 235
         stack2.push(child2, new CommandListenerAdapter());
232 236
         stack2.push(child3, new CommandListenerAdapter() {
@@ -249,7 +253,7 @@ public class NavigatorTest extends BaseTest {
249 253
         disablePushAnimation(child1, child2);
250 254
         disablePopAnimation(child2, child1);
251 255
         StackController stack = newStack(); stack.ensureViewIsCreated();
252
-        uut.setRoot(stack, new CommandListenerAdapter());
256
+        uut.setRoot(stack, new CommandListenerAdapter(), reactInstanceManager);
253 257
         stack.push(child1, new CommandListenerAdapter());
254 258
         stack.push(child2, new CommandListenerAdapter());
255 259
 
@@ -262,7 +266,7 @@ public class NavigatorTest extends BaseTest {
262 266
         StackController stack1 = newStack();
263 267
         StackController stack2 = newStack();
264 268
         BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2));
265
-        uut.setRoot(bottomTabsController, new CommandListenerAdapter());
269
+        uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager);
266 270
 
267 271
         stack1.push(child1, new CommandListenerAdapter());
268 272
         stack2.push(child2, new CommandListenerAdapter());
@@ -282,7 +286,7 @@ public class NavigatorTest extends BaseTest {
282 286
         StackController stack1 = newStack();
283 287
         StackController stack2 = newStack();
284 288
         BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2));
285
-        uut.setRoot(bottomTabsController, new CommandListenerAdapter());
289
+        uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager);
286 290
 
287 291
         stack1.push(child1, new CommandListenerAdapter());
288 292
         stack2.push(child2, new CommandListenerAdapter());
@@ -302,7 +306,7 @@ public class NavigatorTest extends BaseTest {
302 306
         disablePushAnimation(child1, child2, child3);
303 307
 
304 308
         StackController stack = newStack();
305
-        uut.setRoot(stack, new CommandListenerAdapter());
309
+        uut.setRoot(stack, new CommandListenerAdapter(), reactInstanceManager);
306 310
 
307 311
         stack.push(child1, new CommandListenerAdapter());
308 312
         stack.push(child2, new CommandListenerAdapter());
@@ -316,7 +320,7 @@ public class NavigatorTest extends BaseTest {
316 320
         assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
317 321
 
318 322
         ViewController root = spy(child1);
319
-        uut.setRoot(root, new CommandListenerAdapter());
323
+        uut.setRoot(root, new CommandListenerAdapter(), reactInstanceManager);
320 324
         when(root.handleBack(any(CommandListener.class))).thenReturn(true);
321 325
         assertThat(uut.handleBack(new CommandListenerAdapter())).isTrue();
322 326
         verify(root, times(1)).handleBack(any());
@@ -325,7 +329,7 @@ public class NavigatorTest extends BaseTest {
325 329
     @Test
326 330
     public void handleBack_modalTakePrecedenceOverRoot() {
327 331
         ViewController root = spy(child1);
328
-        uut.setRoot(root, new CommandListenerAdapter());
332
+        uut.setRoot(root, new CommandListenerAdapter(), reactInstanceManager);
329 333
         uut.showModal(child2, new CommandListenerAdapter());
330 334
         verify(root, times(0)).handleBack(new CommandListenerAdapter());
331 335
     }
@@ -335,7 +339,7 @@ public class NavigatorTest extends BaseTest {
335 339
         ComponentViewController componentVc = new SimpleComponentViewController(activity, childRegistry, "theId", new Options());
336 340
         componentVc.setParentController(parentController);
337 341
         assertThat(componentVc.options.topBar.title.text.get("")).isEmpty();
338
-        uut.setRoot(componentVc, new CommandListenerAdapter());
342
+        uut.setRoot(componentVc, new CommandListenerAdapter(), reactInstanceManager);
339 343
 
340 344
         Options options = new Options();
341 345
         options.topBar.title.text = new Text("new title");
@@ -368,7 +372,7 @@ public class NavigatorTest extends BaseTest {
368 372
 
369 373
     @Test
370 374
     public void findController_root() {
371
-        uut.setRoot(child1, new CommandListenerAdapter());
375
+        uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager);
372 376
         assertThat(uut.findController(child1.getId())).isEqualTo(child1);
373 377
     }
374 378
 
@@ -399,7 +403,7 @@ public class NavigatorTest extends BaseTest {
399 403
     public void push_promise() {
400 404
         final StackController stackController = newStack();
401 405
         stackController.push(child1, new CommandListenerAdapter());
402
-        uut.setRoot(stackController, new CommandListenerAdapter());
406
+        uut.setRoot(stackController, new CommandListenerAdapter(), reactInstanceManager);
403 407
 
404 408
         assertIsChild(uut.getView(), stackController.getView());
405 409
         assertIsChild(stackController.getView(), child1.getView());
@@ -415,7 +419,7 @@ public class NavigatorTest extends BaseTest {
415 419
 
416 420
     @Test
417 421
     public void push_InvalidPushWithoutAStack_DoesNothing_Promise() {
418
-        uut.setRoot(child1, new CommandListenerAdapter());
422
+        uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager);
419 423
         uut.push(child1.getId(), child2, new CommandListenerAdapter() {
420 424
             @Override
421 425
             public void onError(String message) {
@@ -428,7 +432,7 @@ public class NavigatorTest extends BaseTest {
428 432
     @Test
429 433
     public void pop_InvalidDoesNothing_Promise() {
430 434
         uut.pop("123", Options.EMPTY, new CommandListenerAdapter());
431
-        uut.setRoot(child1, new CommandListenerAdapter());
435
+        uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager);
432 436
         uut.pop(child1.getId(), Options.EMPTY, new CommandListenerAdapter() {
433 437
             @Override
434 438
             public void onError(String reason) {
@@ -442,7 +446,7 @@ public class NavigatorTest extends BaseTest {
442 446
         StackController stack1 = newStack();
443 447
         final StackController stack2 = newStack();
444 448
         BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2));
445
-        uut.setRoot(bottomTabsController, new CommandListenerAdapter());
449
+        uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager);
446 450
 
447 451
         stack1.push(child1, new CommandListenerAdapter());
448 452
         stack2.push(child2, new CommandListenerAdapter());
@@ -458,7 +462,7 @@ public class NavigatorTest extends BaseTest {
458 462
 
459 463
     @Test
460 464
     public void pushIntoModal() {
461
-        uut.setRoot(parentController, new CommandListenerAdapter());
465
+        uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager);
462 466
         StackController stackController = newStack();
463 467
         stackController.push(child1, new CommandListenerAdapter());
464 468
         uut.showModal(stackController, new CommandListenerAdapter());
@@ -473,7 +477,7 @@ public class NavigatorTest extends BaseTest {
473 477
         StackController spy = spy(parentController);
474 478
         StackController parent = newStack();
475 479
         parent.ensureViewIsCreated();
476
-        uut.setRoot(parent, new CommandListenerAdapter());
480
+        uut.setRoot(parent, new CommandListenerAdapter(), reactInstanceManager);
477 481
         parent.push(spy, new CommandListenerAdapter());
478 482
 
479 483
         spy.push(child1, new CommandListenerAdapter());
@@ -505,7 +509,7 @@ public class NavigatorTest extends BaseTest {
505 509
                     }
506 510
                 });
507 511
             }
508
-        });
512
+        }, reactInstanceManager);
509 513
     }
510 514
 
511 515
     @Test
@@ -513,7 +517,7 @@ public class NavigatorTest extends BaseTest {
513 517
         disableShowModalAnimation(child1, child2, child3);
514 518
         disableDismissModalAnimation(child1, child2);
515 519
 
516
-        uut.setRoot(parentController, new CommandListenerAdapter());
520
+        uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager);
517 521
         parentController.push(child3, new CommandListenerAdapter());
518 522
         uut.showModal(child1, new CommandListenerAdapter());
519 523
         uut.showModal(child2, new CommandListenerAdapter());
@@ -532,7 +536,7 @@ public class NavigatorTest extends BaseTest {
532 536
     public void dismissModal_reattachedToRoot() {
533 537
         disableModalAnimations(child1);
534 538
 
535
-        uut.setRoot(parentController, new CommandListenerAdapter());
539
+        uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager);
536 540
         assertThat(ViewUtils.isChildOf(uut.getRootLayout(), parentController.getView()));
537 541
         uut.showModal(child1, new CommandListenerAdapter());
538 542
 
@@ -565,7 +569,7 @@ public class NavigatorTest extends BaseTest {
565 569
         uut.dismissAllModals(Options.EMPTY, new CommandListenerAdapter());
566 570
         verify(parentVisibilityListener, times(0)).onViewAppeared(parentController.getView());
567 571
 
568
-        uut.setRoot(parentController, new CommandListenerAdapter());
572
+        uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager);
569 573
         parentController.push(child2, new CommandListenerAdapter());
570 574
 
571 575
         verify(parentVisibilityListener, times(1)).onViewAppeared(parentController.getView());
@@ -581,7 +585,7 @@ public class NavigatorTest extends BaseTest {
581 585
 
582 586
         parentController.push(child3, new CommandListenerAdapter());
583 587
         StackController spy = spy(parentController);
584
-        uut.setRoot(spy, new CommandListenerAdapter());
588
+        uut.setRoot(spy, new CommandListenerAdapter(), reactInstanceManager);
585 589
         uut.showModal(child1, new CommandListenerAdapter());
586 590
         uut.showModal(child2, new CommandListenerAdapter());
587 591
 
@@ -613,7 +617,7 @@ public class NavigatorTest extends BaseTest {
613 617
 
614 618
         StackController spy = spy(parentController);
615 619
         spy.options.animations.setRoot.enabled = new Bool(false);
616
-        uut.setRoot(spy, new CommandListenerAdapter());
620
+        uut.setRoot(spy, new CommandListenerAdapter(), reactInstanceManager);
617 621
         spy.push(child1, new CommandListenerAdapter());
618 622
         activityController.destroy();
619 623
         verify(spy, times(1)).destroy();
@@ -621,14 +625,14 @@ public class NavigatorTest extends BaseTest {
621 625
 
622 626
     @Test
623 627
     public void destroy_destroyOverlayManager() {
624
-        uut.setRoot(parentController, new CommandListenerAdapter());
628
+        uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager);
625 629
         activityController.destroy();
626 630
         verify(overlayManager).destroy();
627 631
     }
628 632
 
629 633
     @Test
630 634
     public void destroyViews() {
631
-        uut.setRoot(parentController, new CommandListenerAdapter());
635
+        uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager);
632 636
         uut.showModal(child1, new CommandListenerAdapter());
633 637
         uut.showOverlay(child2, new CommandListenerAdapter());
634 638
         uut.destroy();

+ 11
- 6
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java View File

@@ -16,8 +16,11 @@ import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
16 16
 import com.reactnativenavigation.viewcontrollers.ViewController;
17 17
 import com.reactnativenavigation.views.element.ElementTransitionManager;
18 18
 
19
+import com.facebook.react.ReactInstanceManager;
20
+
19 21
 import org.junit.Test;
20 22
 import org.mockito.ArgumentCaptor;
23
+import org.mockito.Mockito;
21 24
 
22 25
 import static org.assertj.core.api.Java6Assertions.assertThat;
23 26
 import static org.mockito.ArgumentMatchers.any;
@@ -34,10 +37,12 @@ public class RootPresenterTest extends BaseTest {
34 37
     private ViewController root;
35 38
     private NavigationAnimator animator;
36 39
     private Options defaultOptions;
40
+    private ReactInstanceManager reactInstanceManager;
37 41
 
38 42
 
39 43
     @Override
40 44
     public void beforeEach() {
45
+        reactInstanceManager = Mockito.mock(ReactInstanceManager.class);
41 46
         Activity activity = newActivity();
42 47
         rootContainer = new FrameLayout(activity);
43 48
         root = new SimpleViewController(activity, new ChildControllersRegistry(), "child1", new Options());
@@ -49,21 +54,21 @@ public class RootPresenterTest extends BaseTest {
49 54
 
50 55
     @Test
51 56
     public void setRoot_viewIsAddedToContainer() {
52
-        uut.setRoot(root, defaultOptions, new CommandListenerAdapter());
57
+        uut.setRoot(root, defaultOptions, new CommandListenerAdapter(), reactInstanceManager);
53 58
         assertThat(root.getView().getParent()).isEqualTo(rootContainer);
54 59
     }
55 60
 
56 61
     @Test
57 62
     public void setRoot_reportsOnSuccess() {
58 63
         CommandListenerAdapter listener = spy(new CommandListenerAdapter());
59
-        uut.setRoot(root, defaultOptions, listener);
64
+        uut.setRoot(root, defaultOptions, listener, reactInstanceManager);
60 65
         verify(listener).onSuccess(root.getId());
61 66
     }
62 67
 
63 68
     @Test
64 69
     public void setRoot_doesNotAnimateByDefault() {
65 70
         CommandListenerAdapter listener = spy(new CommandListenerAdapter());
66
-        uut.setRoot(root, defaultOptions, listener);
71
+        uut.setRoot(root, defaultOptions, listener, reactInstanceManager);
67 72
         verifyZeroInteractions(animator);
68 73
         verify(listener).onSuccess(root.getId());
69 74
     }
@@ -82,7 +87,7 @@ public class RootPresenterTest extends BaseTest {
82 87
         when(spy.resolveCurrentOptions(defaultOptions)).thenReturn(animatedSetRoot);
83 88
         CommandListenerAdapter listener = spy(new CommandListenerAdapter());
84 89
 
85
-        uut.setRoot(spy, defaultOptions, listener);
90
+        uut.setRoot(spy, defaultOptions, listener, reactInstanceManager);
86 91
         verify(animator).setRoot(eq(spy.getView()), eq(animatedSetRoot.animations.setRoot), any());
87 92
         verify(listener).onSuccess(spy.getId());
88 93
     }
@@ -92,7 +97,7 @@ public class RootPresenterTest extends BaseTest {
92 97
         root.options.animations.setRoot.waitForRender = new Bool(true);
93 98
         ViewController spy = spy(root);
94 99
 
95
-        uut.setRoot(spy, defaultOptions, new CommandListenerAdapter());
100
+        uut.setRoot(spy, defaultOptions, new CommandListenerAdapter(), reactInstanceManager);
96 101
 
97 102
         ArgumentCaptor<Bool> captor = ArgumentCaptor.forClass(Bool.class);
98 103
         verify(spy).setWaitForRender(captor.capture());
@@ -105,7 +110,7 @@ public class RootPresenterTest extends BaseTest {
105 110
 
106 111
         ViewController spy = spy(root);
107 112
         CommandListenerAdapter listener = spy(new CommandListenerAdapter());
108
-        uut.setRoot(spy, defaultOptions, listener);
113
+        uut.setRoot(spy, defaultOptions, listener, reactInstanceManager);
109 114
         verify(spy).addOnAppearedListener(any());
110 115
         assertThat(spy.getView().getAlpha()).isZero();
111 116
         verifyZeroInteractions(listener);

+ 18
- 1
lib/ios/RNNCommandsHandler.m View File

@@ -7,6 +7,7 @@
7 7
 #import "RNNErrorHandler.h"
8 8
 #import "RNNDefaultOptionsHelper.h"
9 9
 #import "UIViewController+RNNOptions.h"
10
+#import "React/RCTI18nUtil.h"
10 11
 
11 12
 static NSString* const setRoot	= @"setRoot";
12 13
 static NSString* const setStackRoot	= @"setStackRoot";
@@ -54,7 +55,23 @@ static NSString* const setDefaultOptions	= @"setDefaultOptions";
54 55
 
55 56
 - (void)setRoot:(NSDictionary*)layout completion:(RNNTransitionCompletionBlock)completion {
56 57
 	[self assertReady];
57
-	
58
+
59
+	if (@available(iOS 9, *)) {
60
+	    if(_controllerFactory.defaultOptions.layout.direction.hasValue) {
61
+	        if ([_controllerFactory.defaultOptions.layout.direction.get isEqualToString:@"rtl"]) {
62
+                [[RCTI18nUtil sharedInstance] allowRTL:YES];
63
+                [[RCTI18nUtil sharedInstance] forceRTL:YES];
64
+                [[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceRightToLeft];
65
+                [[UINavigationBar appearance] setSemanticContentAttribute:UISemanticContentAttributeForceRightToLeft];
66
+            } else {
67
+                [[RCTI18nUtil sharedInstance] allowRTL:NO];
68
+                [[RCTI18nUtil sharedInstance] forceRTL:NO];
69
+                [[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceLeftToRight];
70
+                [[UINavigationBar appearance] setSemanticContentAttribute:UISemanticContentAttributeForceLeftToRight];
71
+            }
72
+	    }
73
+	}
74
+
58 75
 	[_modalManager dismissAllModalsAnimated:NO];
59 76
 	[_store removeAllComponentsFromWindow:_mainWindow];
60 77
 	

+ 1
- 0
lib/ios/RNNLayoutOptions.h View File

@@ -3,6 +3,7 @@
3 3
 @interface RNNLayoutOptions : RNNOptions
4 4
 
5 5
 @property (nonatomic, strong) Color* backgroundColor;
6
+@property (nonatomic, strong) Text* direction;
6 7
 @property (nonatomic, strong) id orientation;
7 8
 
8 9
 - (UIInterfaceOrientationMask)supportedOrientations;

+ 2
- 1
lib/ios/RNNLayoutOptions.m View File

@@ -8,8 +8,9 @@
8 8
 	self = [super init];
9 9
 	
10 10
 	self.backgroundColor = [ColorParser parse:dict key:@"backgroundColor"];
11
+	self.direction = [TextParser parse:dict key:@"direction"];
11 12
 	self.orientation = dict[@"orientation"];
12
-	
13
+
13 14
 	return self;
14 15
 }
15 16
 

+ 9
- 0
lib/ios/RNNNavigationStackManager.m View File

@@ -1,5 +1,6 @@
1 1
 #import "RNNNavigationStackManager.h"
2 2
 #import "RNNErrorHandler.h"
3
+#import <React/RCTI18nUtil.h>
3 4
 
4 5
 typedef void (^RNNAnimationBlock)(void);
5 6
 
@@ -8,6 +9,14 @@ typedef void (^RNNAnimationBlock)(void);
8 9
 - (void)push:(UIViewController *)newTop onTop:(UIViewController *)onTopViewController animated:(BOOL)animated animationDelegate:(id)animationDelegate completion:(RNNTransitionCompletionBlock)completion rejection:(RCTPromiseRejectBlock)rejection {
9 10
 	UINavigationController *nvc = onTopViewController.navigationController;
10 11
 
12
+	if([[RCTI18nUtil sharedInstance] isRTL]) {
13
+		nvc.view.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
14
+		nvc.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
15
+	} else {
16
+		nvc.view.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
17
+		nvc.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
18
+	}
19
+
11 20
 	if (animationDelegate) {
12 21
 		nvc.delegate = animationDelegate;
13 22
 	} else {