From d09d0108d1530cf10e24c46efb6c9d9962807ead Mon Sep 17 00:00:00 2001 From: Hadi Mostafapour <33980142+hadimostafapour@users.noreply.github.com> Date: Thu, 14 Feb 2019 14:26:32 +0330 Subject: [PATCH] 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 --- .../parse/LayoutOptions.java | 8 +++ .../react/NavigationModule.java | 2 +- .../TitleBarButtonController.java | 3 +- .../button/NavigationIconResolver.java | 6 +- .../viewcontrollers/navigator/Navigator.java | 6 +- .../navigator/RootPresenter.java | 19 ++++- .../views/titlebar/TitleBar.java | 6 +- .../drawable/ic_arrow_back_black_rtl_24dp.xml | 9 +++ .../button/NavigationIconResolverTest.java | 5 +- .../navigator/NavigatorTest.java | 72 ++++++++++--------- .../navigator/RootPresenterTest.java | 17 +++-- lib/ios/RNNCommandsHandler.m | 19 ++++- lib/ios/RNNLayoutOptions.h | 1 + lib/ios/RNNLayoutOptions.m | 3 +- lib/ios/RNNNavigationStackManager.m | 9 +++ 15 files changed, 132 insertions(+), 53 deletions(-) create mode 100644 lib/android/app/src/main/res/drawable/ic_arrow_back_black_rtl_24dp.xml diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutOptions.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutOptions.java index c12aded0ce7..b10fdbbe9fe 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutOptions.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutOptions.java @@ -4,8 +4,12 @@ import com.reactnativenavigation.parse.params.NullColor; import com.reactnativenavigation.parse.params.NullNumber; import com.reactnativenavigation.parse.params.Number; +import com.reactnativenavigation.parse.params.Text; +import com.reactnativenavigation.parse.params.NullText; import com.reactnativenavigation.parse.parsers.ColorParser; import com.reactnativenavigation.parse.parsers.NumberParser; +import com.reactnativenavigation.parse.parsers.TextParser; + import org.json.JSONObject; @@ -18,6 +22,7 @@ public static LayoutOptions parse(JSONObject json) { result.componentBackgroundColor = ColorParser.parse(json, "componentBackgroundColor"); result.topMargin = NumberParser.parse(json, "topMargin"); result.orientation = OrientationOptions.parse(json); + result.direction = TextParser.parse(json, "direction"); return result; } @@ -26,12 +31,14 @@ public static LayoutOptions parse(JSONObject json) { public Colour componentBackgroundColor = new NullColor(); public Number topMargin = new NullNumber(); public OrientationOptions orientation = new OrientationOptions(); + public Text direction = new NullText(); public void mergeWith(LayoutOptions other) { if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor; if (other.componentBackgroundColor.hasValue()) componentBackgroundColor = other.componentBackgroundColor; if (other.topMargin.hasValue()) topMargin = other.topMargin; if (other.orientation.hasValue()) orientation = other.orientation; + if (other.direction.hasValue()) direction = other.direction; } @@ -40,5 +47,6 @@ public void mergeWithDefault(LayoutOptions defaultOptions) { if (!componentBackgroundColor.hasValue()) componentBackgroundColor = defaultOptions.componentBackgroundColor; if (!topMargin.hasValue()) topMargin = defaultOptions.topMargin; if (!orientation.hasValue()) orientation = defaultOptions.orientation; + if (!direction.hasValue()) direction = defaultOptions.direction; } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java b/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java index 2f2c610c20a..8d7c167ef0a 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java @@ -67,7 +67,7 @@ public void setRoot(String commandId, ReadableMap rawLayoutTree, Promise promise handle(() -> { navigator().setEventEmitter(eventEmitter); final ViewController viewController = newLayoutFactory().create(layoutTree); - navigator().setRoot(viewController, new NativeCommandListener(commandId, promise, eventEmitter, now)); + navigator().setRoot(viewController, new NativeCommandListener(commandId, promise, eventEmitter, now), reactInstanceManager); }); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonController.java index 74cf87d68be..dcf4efee671 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonController.java @@ -102,7 +102,8 @@ public boolean onMenuItemClick(MenuItem item) { } public void applyNavigationIcon(Toolbar toolbar) { - navigationIconResolver.resolve(button, icon -> { + Integer direction = getActivity().getWindow().getDecorView().getLayoutDirection(); + navigationIconResolver.resolve(button, direction, icon -> { setIconColor(icon); toolbar.setNavigationOnClickListener(view -> onPressListener.onPress(button.id)); toolbar.setNavigationIcon(icon); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolver.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolver.java index f4e419968cf..127ee813226 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolver.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolver.java @@ -5,6 +5,7 @@ import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.util.Log; +import android.view.View; import com.reactnativenavigation.R; import com.reactnativenavigation.parse.params.Button; @@ -23,7 +24,7 @@ public NavigationIconResolver(Context context, ImageLoader imageLoader) { this.imageLoader = imageLoader; } - public void resolve(Button button, Func1 onSuccess) { + public void resolve(Button button, Integer direction, Func1 onSuccess) { if (button.icon.hasValue()) { imageLoader.loadIcon(context, button.icon.get(), new ImageLoadingListenerAdapter() { @Override @@ -37,7 +38,8 @@ public void onError(Throwable error) { } }); } else if (Constants.BACK_BUTTON_ID.equals(button.id)) { - onSuccess.run(ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_black_24dp)); + Boolean isRTL = direction == View.LAYOUT_DIRECTION_RTL; + onSuccess.run(ContextCompat.getDrawable(context, isRTL ? R.drawable.ic_arrow_back_black_rtl_24dp : R.drawable.ic_arrow_back_black_24dp)); } else { Log.w("RNN", "Left button needs to have an icon"); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/Navigator.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/Navigator.java index 11cc1d3b10b..82ec475945c 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/Navigator.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/Navigator.java @@ -21,6 +21,8 @@ import com.reactnativenavigation.viewcontrollers.modal.ModalStack; import com.reactnativenavigation.viewcontrollers.stack.StackController; +import com.facebook.react.ReactInstanceManager; + import java.util.Collection; import java.util.Collections; import java.util.List; @@ -125,7 +127,7 @@ public void sendOnNavigationButtonPressed(String buttonId) { } - public void setRoot(final ViewController viewController, CommandListener commandListener) { + public void setRoot(final ViewController viewController, CommandListener commandListener, ReactInstanceManager reactInstanceManager) { destroyRoot(); final boolean removeSplashView = isRootNotCreated(); if (isRootNotCreated()) getView(); @@ -136,7 +138,7 @@ public void onSuccess(String childId) { if (removeSplashView) removePreviousContentView(); super.onSuccess(childId); } - }); + }, reactInstanceManager); } private void removePreviousContentView() { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java index 18b7a3b28f1..965905c6288 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java @@ -2,6 +2,7 @@ import android.content.Context; import android.widget.FrameLayout; +import android.view.View; import com.reactnativenavigation.anim.NavigationAnimator; import com.reactnativenavigation.parse.Options; @@ -9,6 +10,10 @@ import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.views.element.ElementTransitionManager; +import com.facebook.react.modules.i18nmanager.I18nUtil; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.ReactInstanceManager; + public class RootPresenter { private NavigationAnimator animator; private FrameLayout rootLayout; @@ -25,7 +30,8 @@ public RootPresenter(Context context) { this.animator = animator; } - void setRoot(ViewController root, Options defaultOptions, CommandListener listener) { + void setRoot(ViewController root, Options defaultOptions, CommandListener listener, ReactInstanceManager reactInstanceManager) { + setLayoutDirection(root, defaultOptions, (ReactApplicationContext) reactInstanceManager.getCurrentReactContext()); rootLayout.addView(root.getView()); Options options = root.resolveCurrentOptions(defaultOptions); root.setWaitForRender(options.animations.setRoot.waitForRender); @@ -47,4 +53,15 @@ private void animateSetRootAndReportSuccess(ViewController root, CommandListener listener.onSuccess(root.getId()); } } + + private void setLayoutDirection(ViewController root, Options defaultOptions, ReactApplicationContext reactContext) { + if (defaultOptions.layout.direction.hasValue()) { + I18nUtil i18nUtil = I18nUtil.getInstance(); + Boolean isRtl = defaultOptions.layout.direction.get().equals("rtl"); + + root.getActivity().getWindow().getDecorView().setLayoutDirection(isRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); + i18nUtil.allowRTL(reactContext, isRtl); + i18nUtil.forceRTL(reactContext, isRtl); + } + } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java b/lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java index a256dbe80e2..4fb97d4b895 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java @@ -101,6 +101,8 @@ public void setSubtitleAlignment(Alignment alignment) { } private void alignTextView(Alignment alignment, TextView view) { + Integer direction = view.getParent().getLayoutDirection(); + Boolean isRTL = direction == View.LAYOUT_DIRECTION_RTL; int width = view.getResources().getDisplayMetrics().widthPixels; view.post(() -> { if (alignment == Alignment.Center) { @@ -108,9 +110,9 @@ private void alignTextView(Alignment alignment, TextView view) { //noinspection IntegerDivisionInFloatingPointContext view.setX((width - view.getWidth()) / 2); } else if (leftButtonController != null) { - view.setX(getContentInsetStartWithNavigation()); + view.setX(isRTL ? (getWidth() - view.getWidth()) - getContentInsetStartWithNavigation() : getContentInsetStartWithNavigation()); } else { - view.setX(UiUtils.dpToPx(getContext(), DEFAULT_LEFT_MARGIN)); + view.setX(isRTL ? (getWidth() - view.getWidth()) - UiUtils.dpToPx(getContext(), DEFAULT_LEFT_MARGIN) : UiUtils.dpToPx(getContext(), DEFAULT_LEFT_MARGIN)); } }); } diff --git a/lib/android/app/src/main/res/drawable/ic_arrow_back_black_rtl_24dp.xml b/lib/android/app/src/main/res/drawable/ic_arrow_back_black_rtl_24dp.xml new file mode 100644 index 00000000000..1529e1ae5b8 --- /dev/null +++ b/lib/android/app/src/main/res/drawable/ic_arrow_back_black_rtl_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolverTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolverTest.java index fd1804a58bf..7f900ae80a8 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolverTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/button/NavigationIconResolverTest.java @@ -3,6 +3,7 @@ import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.view.View; import com.reactnativenavigation.BaseTest; import com.reactnativenavigation.mocks.ImageLoaderMock; @@ -42,7 +43,7 @@ public void run(Drawable icon) { } }); - uut.resolve(iconButton(), onSuccess); + uut.resolve(iconButton(), View.LAYOUT_DIRECTION_LTR, onSuccess); verify(imageLoader).loadIcon(eq(context), eq(ICON_URI), any()); verify(onSuccess).run(any(Drawable.class)); } @@ -55,7 +56,7 @@ public void run(Drawable param) { } }); - uut.resolve(backButton(), onSuccess); + uut.resolve(backButton(), View.LAYOUT_DIRECTION_LTR, onSuccess); verifyZeroInteractions(imageLoader); verify(onSuccess).run(any()); } diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java index 5ebb39c063c..aaf890cbf58 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java @@ -34,6 +34,8 @@ import com.reactnativenavigation.viewcontrollers.stack.StackController; import com.reactnativenavigation.views.BottomTabs; +import com.facebook.react.ReactInstanceManager; + import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -71,11 +73,13 @@ public class NavigatorTest extends BaseTest { private EventEmitter eventEmitter; private ViewController.ViewVisibilityListener parentVisibilityListener; private ModalStack modalStack; + private ReactInstanceManager reactInstanceManager; @Override public void beforeEach() { childRegistry = new ChildControllersRegistry(); eventEmitter = Mockito.mock(EventEmitter.class); + reactInstanceManager = Mockito.mock(ReactInstanceManager.class); overlayManager = spy(new OverlayManager()); imageLoaderMock = ImageLoaderMock.mock(); activityController = newActivityController(TestActivity.class); @@ -122,7 +126,7 @@ public void setDefaultOptions() { uut.setDefaultOptions(new Options()); SimpleViewController spy = spy(child1); - uut.setRoot(spy, new CommandListenerAdapter()); + uut.setRoot(spy, new CommandListenerAdapter(), reactInstanceManager); Options defaultOptions = new Options(); uut.setDefaultOptions(defaultOptions); @@ -133,9 +137,9 @@ public void setDefaultOptions() { @Test public void setRoot_delegatesToRootPresenter() { CommandListenerAdapter listener = new CommandListenerAdapter(); - uut.setRoot(child1, listener); + uut.setRoot(child1, listener, reactInstanceManager); ArgumentCaptor captor = ArgumentCaptor.forClass(CommandListenerAdapter.class); - verify(rootPresenter).setRoot(eq(child1), eq(uut.getDefaultOptions()), captor.capture()); + verify(rootPresenter).setRoot(eq(child1), eq(uut.getDefaultOptions()), captor.capture(), eq(reactInstanceManager)); assertThat(captor.getValue().getListener()).isEqualTo(listener); } @@ -144,21 +148,21 @@ public void setRoot_clearsSplashLayout() { FrameLayout content = activity.findViewById(android.R.id.content); assertThat(content.getChildCount()).isEqualTo(4); // 3 frame layouts and the default splash layout - uut.setRoot(child2, new CommandListenerAdapter()); + uut.setRoot(child2, new CommandListenerAdapter(), reactInstanceManager); assertThat(content.getChildCount()).isEqualTo(3); } @Test public void setRoot_AddsChildControllerView() { - uut.setRoot(child1, new CommandListenerAdapter()); + uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager); assertIsChild(uut.getRootLayout(), child1.getView()); } @Test public void setRoot_ReplacesExistingChildControllerViews() { - uut.setRoot(child1, new CommandListenerAdapter()); - uut.setRoot(child2, new CommandListenerAdapter()); + uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager); + uut.setRoot(child2, new CommandListenerAdapter(), reactInstanceManager); assertIsChild(uut.getRootLayout(), child2.getView()); } @@ -172,7 +176,7 @@ public void hasUniqueId() { public void push() { StackController stackController = newStack(); stackController.push(child1, new CommandListenerAdapter()); - uut.setRoot(stackController, new CommandListenerAdapter()); + uut.setRoot(stackController, new CommandListenerAdapter(), reactInstanceManager); assertIsChild(uut.getView(), stackController.getView()); assertIsChild(stackController.getView(), child1.getView()); @@ -185,7 +189,7 @@ public void push() { @Test public void push_InvalidPushWithoutAStack_DoesNothing() { - uut.setRoot(child1, new CommandListenerAdapter()); + uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager); uut.push(child1.getId(), child2, new CommandListenerAdapter()); assertIsChild(uut.getView(), child1.getView()); } @@ -197,7 +201,7 @@ public void push_OnCorrectStackByFindingChildId() { stack1.push(child1, new CommandListenerAdapter()); stack2.push(child2, new CommandListenerAdapter()); BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2)); - uut.setRoot(bottomTabsController, new CommandListenerAdapter()); + uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager); SimpleViewController newChild = new SimpleViewController(activity, childRegistry, "new child", tabOptions); uut.push(child2.getId(), newChild, new CommandListenerAdapter()); @@ -216,7 +220,7 @@ public void push_rejectIfNotContainedInStack() { @Test public void pop_InvalidDoesNothing() { uut.pop("123", Options.EMPTY, new CommandListenerAdapter()); - uut.setRoot(child1, new CommandListenerAdapter()); + uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager); uut.pop(child1.getId(), Options.EMPTY, new CommandListenerAdapter()); assertThat(uut.getChildControllers()).hasSize(1); } @@ -226,7 +230,7 @@ public void pop_FromCorrectStackByFindingChildId() { StackController stack1 = newStack(); StackController stack2 = newStack(); BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2)); - uut.setRoot(bottomTabsController, new CommandListenerAdapter()); + uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager); stack1.push(child1, new CommandListenerAdapter()); stack2.push(child2, new CommandListenerAdapter()); stack2.push(child3, new CommandListenerAdapter() { @@ -249,7 +253,7 @@ public void pop_byStackId() { disablePushAnimation(child1, child2); disablePopAnimation(child2, child1); StackController stack = newStack(); stack.ensureViewIsCreated(); - uut.setRoot(stack, new CommandListenerAdapter()); + uut.setRoot(stack, new CommandListenerAdapter(), reactInstanceManager); stack.push(child1, new CommandListenerAdapter()); stack.push(child2, new CommandListenerAdapter()); @@ -262,7 +266,7 @@ public void popTo_FromCorrectStackUpToChild() { StackController stack1 = newStack(); StackController stack2 = newStack(); BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2)); - uut.setRoot(bottomTabsController, new CommandListenerAdapter()); + uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager); stack1.push(child1, new CommandListenerAdapter()); stack2.push(child2, new CommandListenerAdapter()); @@ -282,7 +286,7 @@ public void popToRoot() { StackController stack1 = newStack(); StackController stack2 = newStack(); BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2)); - uut.setRoot(bottomTabsController, new CommandListenerAdapter()); + uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager); stack1.push(child1, new CommandListenerAdapter()); stack2.push(child2, new CommandListenerAdapter()); @@ -302,7 +306,7 @@ public void setStackRoot() { disablePushAnimation(child1, child2, child3); StackController stack = newStack(); - uut.setRoot(stack, new CommandListenerAdapter()); + uut.setRoot(stack, new CommandListenerAdapter(), reactInstanceManager); stack.push(child1, new CommandListenerAdapter()); stack.push(child2, new CommandListenerAdapter()); @@ -316,7 +320,7 @@ public void handleBack_DelegatesToRoot() { assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse(); ViewController root = spy(child1); - uut.setRoot(root, new CommandListenerAdapter()); + uut.setRoot(root, new CommandListenerAdapter(), reactInstanceManager); when(root.handleBack(any(CommandListener.class))).thenReturn(true); assertThat(uut.handleBack(new CommandListenerAdapter())).isTrue(); verify(root, times(1)).handleBack(any()); @@ -325,7 +329,7 @@ public void handleBack_DelegatesToRoot() { @Test public void handleBack_modalTakePrecedenceOverRoot() { ViewController root = spy(child1); - uut.setRoot(root, new CommandListenerAdapter()); + uut.setRoot(root, new CommandListenerAdapter(), reactInstanceManager); uut.showModal(child2, new CommandListenerAdapter()); verify(root, times(0)).handleBack(new CommandListenerAdapter()); } @@ -335,7 +339,7 @@ public void mergeOptions_CallsApplyNavigationOptions() { ComponentViewController componentVc = new SimpleComponentViewController(activity, childRegistry, "theId", new Options()); componentVc.setParentController(parentController); assertThat(componentVc.options.topBar.title.text.get("")).isEmpty(); - uut.setRoot(componentVc, new CommandListenerAdapter()); + uut.setRoot(componentVc, new CommandListenerAdapter(), reactInstanceManager); Options options = new Options(); options.topBar.title.text = new Text("new title"); @@ -368,7 +372,7 @@ public void superCreateItems() { @Test public void findController_root() { - uut.setRoot(child1, new CommandListenerAdapter()); + uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager); assertThat(uut.findController(child1.getId())).isEqualTo(child1); } @@ -399,7 +403,7 @@ private StackController newStack() { public void push_promise() { final StackController stackController = newStack(); stackController.push(child1, new CommandListenerAdapter()); - uut.setRoot(stackController, new CommandListenerAdapter()); + uut.setRoot(stackController, new CommandListenerAdapter(), reactInstanceManager); assertIsChild(uut.getView(), stackController.getView()); assertIsChild(stackController.getView(), child1.getView()); @@ -415,7 +419,7 @@ public void onSuccess(String childId) { @Test public void push_InvalidPushWithoutAStack_DoesNothing_Promise() { - uut.setRoot(child1, new CommandListenerAdapter()); + uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager); uut.push(child1.getId(), child2, new CommandListenerAdapter() { @Override public void onError(String message) { @@ -428,7 +432,7 @@ public void onError(String message) { @Test public void pop_InvalidDoesNothing_Promise() { uut.pop("123", Options.EMPTY, new CommandListenerAdapter()); - uut.setRoot(child1, new CommandListenerAdapter()); + uut.setRoot(child1, new CommandListenerAdapter(), reactInstanceManager); uut.pop(child1.getId(), Options.EMPTY, new CommandListenerAdapter() { @Override public void onError(String reason) { @@ -442,7 +446,7 @@ public void pop_FromCorrectStackByFindingChildId_Promise() { StackController stack1 = newStack(); final StackController stack2 = newStack(); BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2)); - uut.setRoot(bottomTabsController, new CommandListenerAdapter()); + uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager); stack1.push(child1, new CommandListenerAdapter()); stack2.push(child2, new CommandListenerAdapter()); @@ -458,7 +462,7 @@ public void onSuccess(String childId) { @Test public void pushIntoModal() { - uut.setRoot(parentController, new CommandListenerAdapter()); + uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager); StackController stackController = newStack(); stackController.push(child1, new CommandListenerAdapter()); uut.showModal(stackController, new CommandListenerAdapter()); @@ -473,7 +477,7 @@ public void pushedStackCanBePopped() { StackController spy = spy(parentController); StackController parent = newStack(); parent.ensureViewIsCreated(); - uut.setRoot(parent, new CommandListenerAdapter()); + uut.setRoot(parent, new CommandListenerAdapter(), reactInstanceManager); parent.push(spy, new CommandListenerAdapter()); spy.push(child1, new CommandListenerAdapter()); @@ -505,7 +509,7 @@ public void onSuccess(String childId) { } }); } - }); + }, reactInstanceManager); } @Test @@ -513,7 +517,7 @@ public void dismissModal_onViewAppearedInvokedOnRoot() { disableShowModalAnimation(child1, child2, child3); disableDismissModalAnimation(child1, child2); - uut.setRoot(parentController, new CommandListenerAdapter()); + uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager); parentController.push(child3, new CommandListenerAdapter()); uut.showModal(child1, new CommandListenerAdapter()); uut.showModal(child2, new CommandListenerAdapter()); @@ -532,7 +536,7 @@ public void dismissModal_onViewAppearedInvokedOnRoot() { public void dismissModal_reattachedToRoot() { disableModalAnimations(child1); - uut.setRoot(parentController, new CommandListenerAdapter()); + uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager); assertThat(ViewUtils.isChildOf(uut.getRootLayout(), parentController.getView())); uut.showModal(child1, new CommandListenerAdapter()); @@ -565,7 +569,7 @@ public void dismissAllModals_onViewAppearedInvokedOnRoot() { uut.dismissAllModals(Options.EMPTY, new CommandListenerAdapter()); verify(parentVisibilityListener, times(0)).onViewAppeared(parentController.getView()); - uut.setRoot(parentController, new CommandListenerAdapter()); + uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager); parentController.push(child2, new CommandListenerAdapter()); verify(parentVisibilityListener, times(1)).onViewAppeared(parentController.getView()); @@ -581,7 +585,7 @@ public void handleBack_onViewAppearedInvokedOnRoot() { parentController.push(child3, new CommandListenerAdapter()); StackController spy = spy(parentController); - uut.setRoot(spy, new CommandListenerAdapter()); + uut.setRoot(spy, new CommandListenerAdapter(), reactInstanceManager); uut.showModal(child1, new CommandListenerAdapter()); uut.showModal(child2, new CommandListenerAdapter()); @@ -613,7 +617,7 @@ public void destroy_destroyedRoot() { StackController spy = spy(parentController); spy.options.animations.setRoot.enabled = new Bool(false); - uut.setRoot(spy, new CommandListenerAdapter()); + uut.setRoot(spy, new CommandListenerAdapter(), reactInstanceManager); spy.push(child1, new CommandListenerAdapter()); activityController.destroy(); verify(spy, times(1)).destroy(); @@ -621,14 +625,14 @@ public void destroy_destroyedRoot() { @Test public void destroy_destroyOverlayManager() { - uut.setRoot(parentController, new CommandListenerAdapter()); + uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager); activityController.destroy(); verify(overlayManager).destroy(); } @Test public void destroyViews() { - uut.setRoot(parentController, new CommandListenerAdapter()); + uut.setRoot(parentController, new CommandListenerAdapter(), reactInstanceManager); uut.showModal(child1, new CommandListenerAdapter()); uut.showOverlay(child2, new CommandListenerAdapter()); uut.destroy(); diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java index 2d6110b119f..06251c63852 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java @@ -16,8 +16,11 @@ import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.views.element.ElementTransitionManager; +import com.facebook.react.ReactInstanceManager; + import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import static org.assertj.core.api.Java6Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -34,10 +37,12 @@ public class RootPresenterTest extends BaseTest { private ViewController root; private NavigationAnimator animator; private Options defaultOptions; + private ReactInstanceManager reactInstanceManager; @Override public void beforeEach() { + reactInstanceManager = Mockito.mock(ReactInstanceManager.class); Activity activity = newActivity(); rootContainer = new FrameLayout(activity); root = new SimpleViewController(activity, new ChildControllersRegistry(), "child1", new Options()); @@ -49,21 +54,21 @@ public void beforeEach() { @Test public void setRoot_viewIsAddedToContainer() { - uut.setRoot(root, defaultOptions, new CommandListenerAdapter()); + uut.setRoot(root, defaultOptions, new CommandListenerAdapter(), reactInstanceManager); assertThat(root.getView().getParent()).isEqualTo(rootContainer); } @Test public void setRoot_reportsOnSuccess() { CommandListenerAdapter listener = spy(new CommandListenerAdapter()); - uut.setRoot(root, defaultOptions, listener); + uut.setRoot(root, defaultOptions, listener, reactInstanceManager); verify(listener).onSuccess(root.getId()); } @Test public void setRoot_doesNotAnimateByDefault() { CommandListenerAdapter listener = spy(new CommandListenerAdapter()); - uut.setRoot(root, defaultOptions, listener); + uut.setRoot(root, defaultOptions, listener, reactInstanceManager); verifyZeroInteractions(animator); verify(listener).onSuccess(root.getId()); } @@ -82,7 +87,7 @@ public boolean hasAnimation() { when(spy.resolveCurrentOptions(defaultOptions)).thenReturn(animatedSetRoot); CommandListenerAdapter listener = spy(new CommandListenerAdapter()); - uut.setRoot(spy, defaultOptions, listener); + uut.setRoot(spy, defaultOptions, listener, reactInstanceManager); verify(animator).setRoot(eq(spy.getView()), eq(animatedSetRoot.animations.setRoot), any()); verify(listener).onSuccess(spy.getId()); } @@ -92,7 +97,7 @@ public void setRoot_waitForRenderIsSet() { root.options.animations.setRoot.waitForRender = new Bool(true); ViewController spy = spy(root); - uut.setRoot(spy, defaultOptions, new CommandListenerAdapter()); + uut.setRoot(spy, defaultOptions, new CommandListenerAdapter(), reactInstanceManager); ArgumentCaptor captor = ArgumentCaptor.forClass(Bool.class); verify(spy).setWaitForRender(captor.capture()); @@ -105,7 +110,7 @@ public void setRoot_waitForRender() { ViewController spy = spy(root); CommandListenerAdapter listener = spy(new CommandListenerAdapter()); - uut.setRoot(spy, defaultOptions, listener); + uut.setRoot(spy, defaultOptions, listener, reactInstanceManager); verify(spy).addOnAppearedListener(any()); assertThat(spy.getView().getAlpha()).isZero(); verifyZeroInteractions(listener); diff --git a/lib/ios/RNNCommandsHandler.m b/lib/ios/RNNCommandsHandler.m index 3c5930eb84d..8a1b3ee0ae1 100644 --- a/lib/ios/RNNCommandsHandler.m +++ b/lib/ios/RNNCommandsHandler.m @@ -7,6 +7,7 @@ #import "RNNErrorHandler.h" #import "RNNDefaultOptionsHelper.h" #import "UIViewController+RNNOptions.h" +#import "React/RCTI18nUtil.h" static NSString* const setRoot = @"setRoot"; static NSString* const setStackRoot = @"setStackRoot"; @@ -54,7 +55,23 @@ - (instancetype)initWithStore:(RNNStore*)store controllerFactory:(RNNControllerF - (void)setRoot:(NSDictionary*)layout completion:(RNNTransitionCompletionBlock)completion { [self assertReady]; - + + if (@available(iOS 9, *)) { + if(_controllerFactory.defaultOptions.layout.direction.hasValue) { + if ([_controllerFactory.defaultOptions.layout.direction.get isEqualToString:@"rtl"]) { + [[RCTI18nUtil sharedInstance] allowRTL:YES]; + [[RCTI18nUtil sharedInstance] forceRTL:YES]; + [[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceRightToLeft]; + [[UINavigationBar appearance] setSemanticContentAttribute:UISemanticContentAttributeForceRightToLeft]; + } else { + [[RCTI18nUtil sharedInstance] allowRTL:NO]; + [[RCTI18nUtil sharedInstance] forceRTL:NO]; + [[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceLeftToRight]; + [[UINavigationBar appearance] setSemanticContentAttribute:UISemanticContentAttributeForceLeftToRight]; + } + } + } + [_modalManager dismissAllModalsAnimated:NO]; [_store removeAllComponentsFromWindow:_mainWindow]; diff --git a/lib/ios/RNNLayoutOptions.h b/lib/ios/RNNLayoutOptions.h index 708d478981a..952bb31248f 100644 --- a/lib/ios/RNNLayoutOptions.h +++ b/lib/ios/RNNLayoutOptions.h @@ -3,6 +3,7 @@ @interface RNNLayoutOptions : RNNOptions @property (nonatomic, strong) Color* backgroundColor; +@property (nonatomic, strong) Text* direction; @property (nonatomic, strong) id orientation; - (UIInterfaceOrientationMask)supportedOrientations; diff --git a/lib/ios/RNNLayoutOptions.m b/lib/ios/RNNLayoutOptions.m index a5294ce7fa1..63ed8e4bc8e 100644 --- a/lib/ios/RNNLayoutOptions.m +++ b/lib/ios/RNNLayoutOptions.m @@ -8,8 +8,9 @@ - (instancetype)initWithDict:(NSDictionary *)dict { self = [super init]; self.backgroundColor = [ColorParser parse:dict key:@"backgroundColor"]; + self.direction = [TextParser parse:dict key:@"direction"]; self.orientation = dict[@"orientation"]; - + return self; } diff --git a/lib/ios/RNNNavigationStackManager.m b/lib/ios/RNNNavigationStackManager.m index 5f962f9ad5a..fac18dd6f41 100644 --- a/lib/ios/RNNNavigationStackManager.m +++ b/lib/ios/RNNNavigationStackManager.m @@ -1,5 +1,6 @@ #import "RNNNavigationStackManager.h" #import "RNNErrorHandler.h" +#import typedef void (^RNNAnimationBlock)(void); @@ -8,6 +9,14 @@ @implementation RNNNavigationStackManager - (void)push:(UIViewController *)newTop onTop:(UIViewController *)onTopViewController animated:(BOOL)animated animationDelegate:(id)animationDelegate completion:(RNNTransitionCompletionBlock)completion rejection:(RCTPromiseRejectBlock)rejection { UINavigationController *nvc = onTopViewController.navigationController; + if([[RCTI18nUtil sharedInstance] isRTL]) { + nvc.view.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft; + nvc.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft; + } else { + nvc.view.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight; + nvc.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight; + } + if (animationDelegate) { nvc.delegate = animationDelegate; } else {