Browse Source

Apply layout direction directly on views (#5368)

Apply layout direction directly on views

This commit changes how RNN handles layout direction and adds support for LOCALE layout direction on Android.
Until now RNN delegated handling layout direction to RN's I18nUtil. This usually is enough but under certain circumstances
layout direction has to be applied directly on relevant views by RNN - usually when there are conflicts with another dependency
which handles RTL.
Guy Carmeli 5 years ago
parent
commit
fffd2d23f1
No account linked to committer's email address
16 changed files with 236 additions and 141 deletions
  1. 7
    2
      lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java
  2. 53
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutDirection.java
  3. 2
    6
      lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutOptions.java
  4. 0
    5
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/BottomTabPresenter.java
  5. 41
    34
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/BottomTabsPresenter.java
  6. 16
    0
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/LayoutDirectionApplier.java
  7. 9
    22
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/RootPresenter.java
  8. 74
    66
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/StackPresenter.java
  9. 1
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/Navigator.java
  10. 10
    1
      lib/android/app/src/main/java/com/reactnativenavigation/views/BottomTabs.java
  11. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/topbar/TopBar.java
  12. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsPresenterTest.java
  13. 1
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java
  14. 13
    3
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java
  15. 1
    0
      playground/android/app/src/main/AndroidManifest.xml
  16. 2
    1
      playground/src/commons/Options.js

+ 7
- 2
lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java View File

@@ -21,7 +21,7 @@ import com.reactnativenavigation.utils.CommandListenerAdapter;
21 21
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
22 22
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
23 23
 import com.reactnativenavigation.viewcontrollers.navigator.Navigator;
24
-import com.reactnativenavigation.viewcontrollers.navigator.RootPresenter;
24
+import com.reactnativenavigation.presentation.RootPresenter;
25 25
 
26 26
 public class NavigationActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity, JsDevReloadHandler.ReloadListener {
27 27
     @Nullable
@@ -33,7 +33,12 @@ public class NavigationActivity extends AppCompatActivity implements DefaultHard
33 33
     protected void onCreate(@Nullable Bundle savedInstanceState) {
34 34
         super.onCreate(savedInstanceState);
35 35
         addDefaultSplashLayout();
36
-        navigator = new Navigator(this, new ChildControllersRegistry(), new ModalStack(this), new OverlayManager(), new RootPresenter(this));
36
+        navigator = new Navigator(this,
37
+                new ChildControllersRegistry(),
38
+                new ModalStack(this),
39
+                new OverlayManager(),
40
+                new RootPresenter(this)
41
+        );
37 42
         navigator.bindViews();
38 43
         getReactGateway().onActivityCreated(this);
39 44
     }

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

@@ -0,0 +1,53 @@
1
+package com.reactnativenavigation.parse;
2
+
3
+import android.text.TextUtils;
4
+import android.view.View;
5
+
6
+import java.util.Locale;
7
+
8
+public enum LayoutDirection {
9
+    RTL(View.LAYOUT_DIRECTION_RTL),
10
+    LTR(View.LAYOUT_DIRECTION_LTR),
11
+    LOCALE(View.LAYOUT_DIRECTION_LOCALE),
12
+    DEFAULT(View.LAYOUT_DIRECTION_LTR);
13
+
14
+    private final int direction;
15
+
16
+    LayoutDirection(int direction) {
17
+        this.direction = direction;
18
+    }
19
+
20
+    public static LayoutDirection fromString(String direction) {
21
+        switch (direction) {
22
+            case "rtl":
23
+                return RTL;
24
+            case "ltr":
25
+                return LTR;
26
+            case "locale":
27
+                return LOCALE;
28
+            default:
29
+                return DEFAULT;
30
+        }
31
+    }
32
+
33
+    public boolean hasValue() {
34
+        return this != DEFAULT;
35
+    }
36
+
37
+    public int get() {
38
+        return direction;
39
+    }
40
+
41
+    public boolean isRtl() {
42
+        switch (direction) {
43
+            case View.LAYOUT_DIRECTION_LTR:
44
+                return false;
45
+            case View.LAYOUT_DIRECTION_RTL:
46
+                return true;
47
+            case View.LAYOUT_DIRECTION_LOCALE:
48
+                return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
49
+            default:
50
+                return false;
51
+        }
52
+    }
53
+}

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

@@ -4,12 +4,8 @@ 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;
9 7
 import com.reactnativenavigation.parse.parsers.ColorParser;
10 8
 import com.reactnativenavigation.parse.parsers.NumberParser;
11
-import com.reactnativenavigation.parse.parsers.TextParser;
12
-
13 9
 
14 10
 import org.json.JSONObject;
15 11
 
@@ -22,7 +18,7 @@ public class LayoutOptions {
22 18
         result.componentBackgroundColor = ColorParser.parse(json, "componentBackgroundColor");
23 19
         result.topMargin = NumberParser.parse(json, "topMargin");
24 20
         result.orientation = OrientationOptions.parse(json);
25
-        result.direction = TextParser.parse(json, "direction");
21
+        result.direction = LayoutDirection.fromString(json.optString("direction", ""));
26 22
 
27 23
         return result;
28 24
     }
@@ -31,7 +27,7 @@ public class LayoutOptions {
31 27
     public Colour componentBackgroundColor = new NullColor();
32 28
     public Number topMargin = new NullNumber();
33 29
     public OrientationOptions orientation = new OrientationOptions();
34
-    public Text direction = new NullText();
30
+    public LayoutDirection direction = LayoutDirection.DEFAULT;
35 31
 
36 32
     public void mergeWith(LayoutOptions other) {
37 33
         if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;

+ 0
- 5
lib/android/app/src/main/java/com/reactnativenavigation/presentation/BottomTabPresenter.java View File

@@ -3,7 +3,6 @@ package com.reactnativenavigation.presentation;
3 3
 import android.content.Context;
4 4
 import android.graphics.drawable.Drawable;
5 5
 import android.support.annotation.NonNull;
6
-import android.support.v4.content.ContextCompat;
7 6
 
8 7
 import com.aurelhubert.ahbottomnavigation.notification.AHNotification;
9 8
 import com.reactnativenavigation.parse.BottomTabOptions;
@@ -26,8 +25,6 @@ public class BottomTabPresenter {
26 25
     private Options defaultOptions;
27 26
     private final BottomTabFinder bottomTabFinder;
28 27
     private BottomTabs bottomTabs;
29
-    private final int defaultSelectedTextColor;
30
-    private final int defaultTextColor;
31 28
     private final List<ViewController> tabs;
32 29
     private final int defaultDotIndicatorSize;
33 30
 
@@ -37,8 +34,6 @@ public class BottomTabPresenter {
37 34
         this.bottomTabFinder = new BottomTabFinder(tabs);
38 35
         this.imageLoader = imageLoader;
39 36
         this.defaultOptions = defaultOptions;
40
-        defaultSelectedTextColor = defaultOptions.bottomTabOptions.selectedIconColor.get(ContextCompat.getColor(context, com.aurelhubert.ahbottomnavigation.R.color.colorBottomNavigationAccent));
41
-        defaultTextColor = defaultOptions.bottomTabOptions.iconColor.get(ContextCompat.getColor(context, com.aurelhubert.ahbottomnavigation.R.color.colorBottomNavigationInactive));
42 37
         defaultDotIndicatorSize = dpToPx(context, 6);
43 38
     }
44 39
 

+ 41
- 34
lib/android/app/src/main/java/com/reactnativenavigation/presentation/BottomTabsPresenter.java View File

@@ -50,56 +50,59 @@ public class BottomTabsPresenter {
50 50
     }
51 51
 
52 52
     public void mergeOptions(Options options) {
53
-        mergeBottomTabsOptions(options.bottomTabsOptions, options.animations);
53
+        mergeBottomTabsOptions(options);
54 54
     }
55 55
 
56 56
     public void applyOptions(Options options) {
57
-        Options withDefaultOptions = options.copy().withDefaultOptions(defaultOptions);
58
-        applyBottomTabsOptions(withDefaultOptions.bottomTabsOptions, withDefaultOptions.animations);
57
+        applyBottomTabsOptions(options.copy().withDefaultOptions(defaultOptions));
59 58
     }
60 59
 
61 60
     public void applyChildOptions(Options options, Component child) {
62 61
         int tabIndex = bottomTabFinder.findByComponent(child);
63 62
         if (tabIndex >= 0) {
64 63
             Options withDefaultOptions = options.copy().withDefaultOptions(defaultOptions);
65
-            applyBottomTabsOptions(withDefaultOptions.bottomTabsOptions, withDefaultOptions.animations);
64
+            applyBottomTabsOptions(withDefaultOptions);
66 65
             applyDrawBehind(withDefaultOptions.bottomTabsOptions, tabIndex);
67 66
         }
68 67
     }
69 68
 
70 69
     public void mergeChildOptions(Options options, Component child) {
71
-        mergeBottomTabsOptions(options.bottomTabsOptions, options.animations);
70
+        mergeBottomTabsOptions(options);
72 71
         int tabIndex = bottomTabFinder.findByComponent(child);
73 72
         if (tabIndex >= 0) mergeDrawBehind(options.bottomTabsOptions, tabIndex);
74 73
     }
75 74
 
76
-    private void mergeBottomTabsOptions(BottomTabsOptions options, AnimationsOptions animations) {
77
-        if (options.titleDisplayMode.hasValue()) {
78
-            bottomTabs.setTitleState(options.titleDisplayMode.toState());
75
+    private void mergeBottomTabsOptions(Options options) {
76
+        BottomTabsOptions bottomTabsOptions = options.bottomTabsOptions;
77
+        AnimationsOptions animations = options.animations;
78
+
79
+        if (options.layout.direction.hasValue()) bottomTabs.setLayoutDirection(options.layout.direction);
80
+        if (bottomTabsOptions.titleDisplayMode.hasValue()) {
81
+            bottomTabs.setTitleState(bottomTabsOptions.titleDisplayMode.toState());
79 82
         }
80
-        if (options.backgroundColor.hasValue()) {
81
-            bottomTabs.setBackgroundColor(options.backgroundColor.get());
83
+        if (bottomTabsOptions.backgroundColor.hasValue()) {
84
+            bottomTabs.setBackgroundColor(bottomTabsOptions.backgroundColor.get());
82 85
         }
83
-        if (options.currentTabIndex.hasValue()) {
84
-            int tabIndex = options.currentTabIndex.get();
86
+        if (bottomTabsOptions.currentTabIndex.hasValue()) {
87
+            int tabIndex = bottomTabsOptions.currentTabIndex.get();
85 88
             if (tabIndex >= 0) tabSelector.selectTab(tabIndex);
86 89
         }
87
-        if (options.testId.hasValue()) {
88
-            bottomTabs.setTag(options.testId.get());
90
+        if (bottomTabsOptions.testId.hasValue()) {
91
+            bottomTabs.setTag(bottomTabsOptions.testId.get());
89 92
         }
90
-        if (options.currentTabId.hasValue()) {
91
-            int tabIndex = bottomTabFinder.findByControllerId(options.currentTabId.get());
93
+        if (bottomTabsOptions.currentTabId.hasValue()) {
94
+            int tabIndex = bottomTabFinder.findByControllerId(bottomTabsOptions.currentTabId.get());
92 95
             if (tabIndex >= 0) tabSelector.selectTab(tabIndex);
93 96
         }
94
-        if (options.visible.isTrue()) {
95
-            if (options.animate.isTrueOrUndefined()) {
97
+        if (bottomTabsOptions.visible.isTrue()) {
98
+            if (bottomTabsOptions.animate.isTrueOrUndefined()) {
96 99
                 animator.show(animations);
97 100
             } else {
98 101
                 bottomTabs.restoreBottomNavigation(false);
99 102
             }
100 103
         }
101
-        if (options.visible.isFalse()) {
102
-            if (options.animate.isTrueOrUndefined()) {
104
+        if (bottomTabsOptions.visible.isFalse()) {
105
+            if (bottomTabsOptions.animate.isTrueOrUndefined()) {
103 106
                 animator.hide(animations);
104 107
             } else {
105 108
                 bottomTabs.hideBottomNavigation(false);
@@ -127,34 +130,38 @@ public class BottomTabsPresenter {
127 130
         }
128 131
     }
129 132
 
130
-    private void applyBottomTabsOptions(BottomTabsOptions options, AnimationsOptions animationsOptions) {
131
-        bottomTabs.setTitleState(options.titleDisplayMode.get(TitleState.SHOW_WHEN_ACTIVE));
132
-        bottomTabs.setBackgroundColor(options.backgroundColor.get(Color.WHITE));
133
-        if (options.currentTabIndex.hasValue()) {
134
-            int tabIndex = options.currentTabIndex.get();
133
+    private void applyBottomTabsOptions(Options options) {
134
+        BottomTabsOptions bottomTabsOptions = options.bottomTabsOptions;
135
+        AnimationsOptions animationsOptions = options.animations;
136
+
137
+        bottomTabs.setLayoutDirection(options.layout.direction);
138
+        bottomTabs.setTitleState(bottomTabsOptions.titleDisplayMode.get(TitleState.SHOW_WHEN_ACTIVE));
139
+        bottomTabs.setBackgroundColor(bottomTabsOptions.backgroundColor.get(Color.WHITE));
140
+        if (bottomTabsOptions.currentTabIndex.hasValue()) {
141
+            int tabIndex = bottomTabsOptions.currentTabIndex.get();
135 142
             if (tabIndex >= 0) tabSelector.selectTab(tabIndex);
136 143
         }
137
-        if (options.testId.hasValue()) bottomTabs.setTag(options.testId.get());
138
-        if (options.currentTabId.hasValue()) {
139
-            int tabIndex = bottomTabFinder.findByControllerId(options.currentTabId.get());
144
+        if (bottomTabsOptions.testId.hasValue()) bottomTabs.setTag(bottomTabsOptions.testId.get());
145
+        if (bottomTabsOptions.currentTabId.hasValue()) {
146
+            int tabIndex = bottomTabFinder.findByControllerId(bottomTabsOptions.currentTabId.get());
140 147
             if (tabIndex >= 0) tabSelector.selectTab(tabIndex);
141 148
         }
142
-        if (options.visible.isTrueOrUndefined()) {
143
-            if (options.animate.isTrueOrUndefined()) {
149
+        if (bottomTabsOptions.visible.isTrueOrUndefined()) {
150
+            if (bottomTabsOptions.animate.isTrueOrUndefined()) {
144 151
                 animator.show(animationsOptions);
145 152
             } else {
146 153
                 bottomTabs.restoreBottomNavigation(false);
147 154
             }
148 155
         }
149
-        if (options.visible.isFalse()) {
150
-            if (options.animate.isTrueOrUndefined()) {
156
+        if (bottomTabsOptions.visible.isFalse()) {
157
+            if (bottomTabsOptions.animate.isTrueOrUndefined()) {
151 158
                 animator.hide(animationsOptions);
152 159
             } else {
153 160
                 bottomTabs.hideBottomNavigation(false);
154 161
             }
155 162
         }
156
-        if (options.elevation.hasValue()) {
157
-            bottomTabs.setUseElevation(true, options.elevation.get().floatValue());
163
+        if (bottomTabsOptions.elevation.hasValue()) {
164
+            bottomTabs.setUseElevation(true, bottomTabsOptions.elevation.get().floatValue());
158 165
         }
159 166
     }
160 167
 }

+ 16
- 0
lib/android/app/src/main/java/com/reactnativenavigation/presentation/LayoutDirectionApplier.java View File

@@ -0,0 +1,16 @@
1
+package com.reactnativenavigation.presentation;
2
+
3
+import com.facebook.react.ReactInstanceManager;
4
+import com.facebook.react.modules.i18nmanager.I18nUtil;
5
+import com.reactnativenavigation.parse.Options;
6
+import com.reactnativenavigation.viewcontrollers.ViewController;
7
+
8
+public class LayoutDirectionApplier {
9
+    public void apply(ViewController root, Options options, ReactInstanceManager instanceManager) {
10
+        if (options.layout.direction.hasValue() && instanceManager.getCurrentReactContext() != null) {
11
+            root.getActivity().getWindow().getDecorView().setLayoutDirection(options.layout.direction.get());
12
+            I18nUtil.getInstance().allowRTL(instanceManager.getCurrentReactContext(), options.layout.direction.isRtl());
13
+            I18nUtil.getInstance().forceRTL(instanceManager.getCurrentReactContext(), options.layout.direction.isRtl());
14
+        }
15
+    }
16
+}

lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java → lib/android/app/src/main/java/com/reactnativenavigation/presentation/RootPresenter.java View File

@@ -1,37 +1,35 @@
1
-package com.reactnativenavigation.viewcontrollers.navigator;
1
+package com.reactnativenavigation.presentation;
2 2
 
3 3
 import android.content.Context;
4 4
 import android.widget.FrameLayout;
5
-import android.view.View;
6 5
 
6
+import com.facebook.react.ReactInstanceManager;
7 7
 import com.reactnativenavigation.anim.NavigationAnimator;
8 8
 import com.reactnativenavigation.parse.Options;
9 9
 import com.reactnativenavigation.utils.CommandListener;
10 10
 import com.reactnativenavigation.viewcontrollers.ViewController;
11 11
 import com.reactnativenavigation.views.element.ElementTransitionManager;
12 12
 
13
-import com.facebook.react.modules.i18nmanager.I18nUtil;
14
-import com.facebook.react.bridge.ReactApplicationContext;
15
-import com.facebook.react.ReactInstanceManager;
16
-
17 13
 public class RootPresenter {
18 14
     private NavigationAnimator animator;
15
+    private LayoutDirectionApplier layoutDirectionApplier;
19 16
     private FrameLayout rootLayout;
20 17
 
21
-    void setRootContainer(FrameLayout rootLayout) {
18
+    public void setRootContainer(FrameLayout rootLayout) {
22 19
         this.rootLayout = rootLayout;
23 20
     }
24 21
 
25 22
     public RootPresenter(Context context) {
26
-        animator = new NavigationAnimator(context, new ElementTransitionManager());
23
+        this(new NavigationAnimator(context, new ElementTransitionManager()), new LayoutDirectionApplier());
27 24
     }
28 25
 
29
-    RootPresenter(NavigationAnimator animator) {
26
+    public RootPresenter(NavigationAnimator animator, LayoutDirectionApplier layoutDirectionApplier) {
30 27
         this.animator = animator;
28
+        this.layoutDirectionApplier = layoutDirectionApplier;
31 29
     }
32 30
 
33
-    void setRoot(ViewController root, Options defaultOptions, CommandListener listener, ReactInstanceManager reactInstanceManager) {
34
-        setLayoutDirection(root, defaultOptions, (ReactApplicationContext) reactInstanceManager.getCurrentReactContext());
31
+    public void setRoot(ViewController root, Options defaultOptions, CommandListener listener, ReactInstanceManager reactInstanceManager) {
32
+        layoutDirectionApplier.apply(root, defaultOptions, reactInstanceManager);
35 33
         rootLayout.addView(root.getView());
36 34
         Options options = root.resolveCurrentOptions(defaultOptions);
37 35
         root.setWaitForRender(options.animations.setRoot.waitForRender);
@@ -53,15 +51,4 @@ public class RootPresenter {
53 51
             listener.onSuccess(root.getId());
54 52
         }
55 53
     }
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
-    }
67 54
 }

+ 74
- 66
lib/android/app/src/main/java/com/reactnativenavigation/presentation/StackPresenter.java View File

@@ -127,7 +127,7 @@ public class StackPresenter {
127 127
     public void mergeOptions(Options options, Component currentChild) {
128 128
         mergeOrientation(options.layout.orientation);
129 129
 //        mergeButtons(topBar, withDefault.topBar.buttons, child);
130
-        mergeTopBarOptions(options.topBar, options.animations, currentChild);
130
+        mergeTopBarOptions(options, currentChild);
131 131
         mergeTopTabsOptions(options.topTabs);
132 132
         mergeTopTabOptions(options.topTabOptions);
133 133
     }
@@ -141,7 +141,7 @@ public class StackPresenter {
141 141
         Options withDefault = options.copy().withDefaultOptions(defaultOptions);
142 142
         applyOrientation(withDefault.layout.orientation);
143 143
         applyButtons(withDefault.topBar, child);
144
-        applyTopBarOptions(withDefault.topBar, withDefault.animations, child, options);
144
+        applyTopBarOptions(withDefault, child, options);
145 145
         applyTopTabsOptions(withDefault.topTabs);
146 146
         applyTopTabOptions(withDefault.topTabOptions);
147 147
     }
@@ -164,55 +164,59 @@ public class StackPresenter {
164 164
         if (buttons != null) forEach(buttons.values(), ViewController::destroy);
165 165
     }
166 166
 
167
-    private void applyTopBarOptions(TopBarOptions options, AnimationsOptions animationOptions, Component component, Options componentOptions) {
168
-        topBar.setHeight(options.height.get(UiUtils.getTopBarHeightDp(activity)));
169
-        topBar.setElevation(options.elevation.get(DEFAULT_ELEVATION));
167
+    private void applyTopBarOptions(Options options, Component component, Options componentOptions) {
168
+        TopBarOptions topBarOptions = options.topBar;
169
+        AnimationsOptions animationOptions = options.animations;
170
+
171
+        topBar.setLayoutDirection(options.layout.direction);
172
+        topBar.setHeight(topBarOptions.height.get(UiUtils.getTopBarHeightDp(activity)));
173
+        topBar.setElevation(topBarOptions.elevation.get(DEFAULT_ELEVATION));
170 174
         if (topBar.getLayoutParams() instanceof MarginLayoutParams) {
171
-            ((MarginLayoutParams) topBar.getLayoutParams()).topMargin = UiUtils.dpToPx(activity, options.topMargin.get(0));
175
+            ((MarginLayoutParams) topBar.getLayoutParams()).topMargin = UiUtils.dpToPx(activity, topBarOptions.topMargin.get(0));
172 176
         }
173 177
 
174
-        topBar.setTitleHeight(options.title.height.get(UiUtils.getTopBarHeightDp(activity)));
175
-        topBar.setTitle(options.title.text.get(""));
176
-        topBar.setTitleTopMargin(options.title.topMargin.get(0));
178
+        topBar.setTitleHeight(topBarOptions.title.height.get(UiUtils.getTopBarHeightDp(activity)));
179
+        topBar.setTitle(topBarOptions.title.text.get(""));
180
+        topBar.setTitleTopMargin(topBarOptions.title.topMargin.get(0));
177 181
 
178
-        if (options.title.component.hasValue()) {
182
+        if (topBarOptions.title.component.hasValue()) {
179 183
             if (titleControllers.containsKey(component)) {
180 184
                 topBar.setTitleComponent(titleControllers.get(component).getView());
181 185
             } else {
182 186
                 TitleBarReactViewController controller = new TitleBarReactViewController(activity, titleViewCreator);
183
-                controller.setWaitForRender(options.title.component.waitForRender);
187
+                controller.setWaitForRender(topBarOptions.title.component.waitForRender);
184 188
                 titleControllers.put(component, controller);
185
-                controller.setComponent(options.title.component);
186
-                controller.getView().setLayoutParams(getComponentLayoutParams(options.title.component));
189
+                controller.setComponent(topBarOptions.title.component);
190
+                controller.getView().setLayoutParams(getComponentLayoutParams(topBarOptions.title.component));
187 191
                 topBar.setTitleComponent(controller.getView());
188 192
             }
189 193
         }
190 194
 
191
-        topBar.setTitleFontSize(options.title.fontSize.get(defaultTitleFontSize));
192
-        topBar.setTitleTextColor(options.title.color.get(DEFAULT_TITLE_COLOR));
193
-        topBar.setTitleTypeface(options.title.fontFamily);
194
-        topBar.setTitleAlignment(options.title.alignment);
195
+        topBar.setTitleFontSize(topBarOptions.title.fontSize.get(defaultTitleFontSize));
196
+        topBar.setTitleTextColor(topBarOptions.title.color.get(DEFAULT_TITLE_COLOR));
197
+        topBar.setTitleTypeface(topBarOptions.title.fontFamily);
198
+        topBar.setTitleAlignment(topBarOptions.title.alignment);
195 199
 
196
-        topBar.setSubtitle(options.subtitle.text.get(""));
197
-        topBar.setSubtitleFontSize(options.subtitle.fontSize.get(defaultSubtitleFontSize));
198
-        topBar.setSubtitleColor(options.subtitle.color.get(DEFAULT_SUBTITLE_COLOR));
199
-        topBar.setSubtitleFontFamily(options.subtitle.fontFamily);
200
-        topBar.setSubtitleAlignment(options.subtitle.alignment);
200
+        topBar.setSubtitle(topBarOptions.subtitle.text.get(""));
201
+        topBar.setSubtitleFontSize(topBarOptions.subtitle.fontSize.get(defaultSubtitleFontSize));
202
+        topBar.setSubtitleColor(topBarOptions.subtitle.color.get(DEFAULT_SUBTITLE_COLOR));
203
+        topBar.setSubtitleFontFamily(topBarOptions.subtitle.fontFamily);
204
+        topBar.setSubtitleAlignment(topBarOptions.subtitle.alignment);
201 205
 
202
-        topBar.setBorderHeight(options.borderHeight.get(0d));
203
-        topBar.setBorderColor(options.borderColor.get(DEFAULT_BORDER_COLOR));
206
+        topBar.setBorderHeight(topBarOptions.borderHeight.get(0d));
207
+        topBar.setBorderColor(topBarOptions.borderColor.get(DEFAULT_BORDER_COLOR));
204 208
 
205
-        topBar.setBackgroundColor(options.background.color.get(Color.WHITE));
209
+        topBar.setBackgroundColor(topBarOptions.background.color.get(Color.WHITE));
206 210
 
207
-        if (options.background.component.hasValue()) {
208
-            View createdComponent = findBackgroundComponent(options.background.component);
211
+        if (topBarOptions.background.component.hasValue()) {
212
+            View createdComponent = findBackgroundComponent(topBarOptions.background.component);
209 213
             if (createdComponent != null) {
210 214
                 topBar.setBackgroundComponent(createdComponent);
211 215
             } else {
212 216
                 TopBarBackgroundViewController controller = new TopBarBackgroundViewController(activity, topBarBackgroundViewCreator);
213
-                controller.setWaitForRender(options.background.waitForRender);
217
+                controller.setWaitForRender(topBarOptions.background.waitForRender);
214 218
                 backgroundControllers.put(component, controller);
215
-                controller.setComponent(options.background.component);
219
+                controller.setComponent(topBarOptions.background.component);
216 220
                 controller.getView().setLayoutParams(new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
217 221
                 topBar.setBackgroundComponent(controller.getView());
218 222
             }
@@ -220,18 +224,18 @@ public class StackPresenter {
220 224
             topBar.clearBackgroundComponent();
221 225
         }
222 226
 
223
-        if (options.testId.hasValue()) topBar.setTestId(options.testId.get());
224
-        applyTopBarVisibility(options, animationOptions, componentOptions);
225
-        if (options.drawBehind.isTrue() && !componentOptions.layout.topMargin.hasValue()) {
227
+        if (topBarOptions.testId.hasValue()) topBar.setTestId(topBarOptions.testId.get());
228
+        applyTopBarVisibility(topBarOptions, animationOptions, componentOptions);
229
+        if (topBarOptions.drawBehind.isTrue() && !componentOptions.layout.topMargin.hasValue()) {
226 230
             component.drawBehindTopBar();
227
-        } else if (options.drawBehind.isFalseOrUndefined()) {
231
+        } else if (topBarOptions.drawBehind.isFalseOrUndefined()) {
228 232
             component.drawBelowTopBar(topBar);
229 233
         }
230
-        if (options.hideOnScroll.isTrue()) {
234
+        if (topBarOptions.hideOnScroll.isTrue()) {
231 235
             if (component instanceof IReactView) {
232 236
                 topBar.enableCollapse(((IReactView) component).getScrollEventListener());
233 237
             }
234
-        } else if (options.hideOnScroll.isFalseOrUndefined()) {
238
+        } else if (topBarOptions.hideOnScroll.isFalseOrUndefined()) {
235 239
             topBar.disableCollapse();
236 240
         }
237 241
     }
@@ -351,7 +355,7 @@ public class StackPresenter {
351 355
         TopBarOptions topBar = toMerge.copy().mergeWith(resolvedOptions).withDefaultOptions(defaultOptions).topBar;
352 356
         mergeOrientation(toMerge.layout.orientation);
353 357
         mergeButtons(topBar, toMerge.topBar.buttons, child);
354
-        mergeTopBarOptions(toMerge.topBar, toMerge.animations, child);
358
+        mergeTopBarOptions(toMerge, child);
355 359
         mergeTopTabsOptions(toMerge.topTabs);
356 360
         mergeTopTabOptions(toMerge.topTabOptions);
357 361
     }
@@ -403,78 +407,82 @@ public class StackPresenter {
403 407
         return result;
404 408
     }
405 409
 
406
-    private void mergeTopBarOptions(TopBarOptions options, AnimationsOptions animationsOptions, Component component) {
407
-        if (options.height.hasValue()) topBar.setHeight(options.height.get());
408
-        if (options.elevation.hasValue()) topBar.setElevation(options.elevation.get());
409
-        if (options.topMargin.hasValue() && topBar.getLayoutParams() instanceof MarginLayoutParams) {
410
-            ((MarginLayoutParams) topBar.getLayoutParams()).topMargin = UiUtils.dpToPx(activity, options.topMargin.get());
410
+    private void mergeTopBarOptions(Options options, Component component) {
411
+        TopBarOptions topBarOptions = options.topBar;
412
+        AnimationsOptions animationsOptions = options.animations;
413
+
414
+        if (options.layout.direction.hasValue()) topBar.setLayoutDirection(options.layout.direction);
415
+        if (topBarOptions.height.hasValue()) topBar.setHeight(topBarOptions.height.get());
416
+        if (topBarOptions.elevation.hasValue()) topBar.setElevation(topBarOptions.elevation.get());
417
+        if (topBarOptions.topMargin.hasValue() && topBar.getLayoutParams() instanceof MarginLayoutParams) {
418
+            ((MarginLayoutParams) topBar.getLayoutParams()).topMargin = UiUtils.dpToPx(activity, topBarOptions.topMargin.get());
411 419
         }
412 420
 
413
-        if (options.title.height.hasValue()) topBar.setTitleHeight(options.title.height.get());
414
-        if (options.title.text.hasValue()) topBar.setTitle(options.title.text.get());
415
-        if (options.title.topMargin.hasValue()) topBar.setTitleTopMargin(options.title.topMargin.get());
421
+        if (topBarOptions.title.height.hasValue()) topBar.setTitleHeight(topBarOptions.title.height.get());
422
+        if (topBarOptions.title.text.hasValue()) topBar.setTitle(topBarOptions.title.text.get());
423
+        if (topBarOptions.title.topMargin.hasValue()) topBar.setTitleTopMargin(topBarOptions.title.topMargin.get());
416 424
 
417
-        if (options.title.component.hasValue()) {
425
+        if (topBarOptions.title.component.hasValue()) {
418 426
             if (titleControllers.containsKey(component)) {
419 427
                 topBar.setTitleComponent(titleControllers.get(component).getView());
420 428
             } else {
421 429
                 TitleBarReactViewController controller = new TitleBarReactViewController(activity, titleViewCreator);
422 430
                 titleControllers.put(component, controller);
423
-                controller.setComponent(options.title.component);
424
-                controller.getView().setLayoutParams(getComponentLayoutParams(options.title.component));
431
+                controller.setComponent(topBarOptions.title.component);
432
+                controller.getView().setLayoutParams(getComponentLayoutParams(topBarOptions.title.component));
425 433
                 topBar.setTitleComponent(controller.getView());
426 434
             }
427 435
         }
428 436
 
429
-        if (options.title.color.hasValue()) topBar.setTitleTextColor(options.title.color.get());
430
-        if (options.title.fontSize.hasValue()) topBar.setTitleFontSize(options.title.fontSize.get());
431
-        if (options.title.fontFamily != null) topBar.setTitleTypeface(options.title.fontFamily);
437
+        if (topBarOptions.title.color.hasValue()) topBar.setTitleTextColor(topBarOptions.title.color.get());
438
+        if (topBarOptions.title.fontSize.hasValue()) topBar.setTitleFontSize(topBarOptions.title.fontSize.get());
439
+        if (topBarOptions.title.fontFamily != null) topBar.setTitleTypeface(topBarOptions.title.fontFamily);
432 440
 
433
-        if (options.subtitle.text.hasValue()) topBar.setSubtitle(options.subtitle.text.get());
434
-        if (options.subtitle.color.hasValue()) topBar.setSubtitleColor(options.subtitle.color.get());
435
-        if (options.subtitle.fontSize.hasValue()) topBar.setSubtitleFontSize(options.subtitle.fontSize.get());
436
-        if (options.subtitle.fontFamily != null) topBar.setSubtitleFontFamily(options.subtitle.fontFamily);
441
+        if (topBarOptions.subtitle.text.hasValue()) topBar.setSubtitle(topBarOptions.subtitle.text.get());
442
+        if (topBarOptions.subtitle.color.hasValue()) topBar.setSubtitleColor(topBarOptions.subtitle.color.get());
443
+        if (topBarOptions.subtitle.fontSize.hasValue()) topBar.setSubtitleFontSize(topBarOptions.subtitle.fontSize.get());
444
+        if (topBarOptions.subtitle.fontFamily != null) topBar.setSubtitleFontFamily(topBarOptions.subtitle.fontFamily);
437 445
 
438
-        if (options.background.color.hasValue()) topBar.setBackgroundColor(options.background.color.get());
446
+        if (topBarOptions.background.color.hasValue()) topBar.setBackgroundColor(topBarOptions.background.color.get());
439 447
 
440
-        if (options.background.component.hasValue()) {
448
+        if (topBarOptions.background.component.hasValue()) {
441 449
             if (backgroundControllers.containsKey(component)) {
442 450
                 topBar.setBackgroundComponent(backgroundControllers.get(component).getView());
443 451
             } else {
444 452
                 TopBarBackgroundViewController controller = new TopBarBackgroundViewController(activity, topBarBackgroundViewCreator);
445 453
                 backgroundControllers.put(component, controller);
446
-                controller.setComponent(options.background.component);
454
+                controller.setComponent(topBarOptions.background.component);
447 455
                 controller.getView().setLayoutParams(new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
448 456
                 topBar.setBackgroundComponent(controller.getView());
449 457
             }
450 458
         }
451 459
 
452
-        if (options.testId.hasValue()) topBar.setTestId(options.testId.get());
460
+        if (topBarOptions.testId.hasValue()) topBar.setTestId(topBarOptions.testId.get());
453 461
 
454
-        if (options.visible.isFalse()) {
455
-            if (options.animate.isTrueOrUndefined()) {
462
+        if (topBarOptions.visible.isFalse()) {
463
+            if (topBarOptions.animate.isTrueOrUndefined()) {
456 464
                 topBar.hideAnimate(animationsOptions.pop.topBar);
457 465
             } else {
458 466
                 topBar.hide();
459 467
             }
460 468
         }
461
-        if (options.visible.isTrue()) {
462
-            if (options.animate.isTrueOrUndefined()) {
469
+        if (topBarOptions.visible.isTrue()) {
470
+            if (topBarOptions.animate.isTrueOrUndefined()) {
463 471
                 topBar.showAnimate(animationsOptions.push.topBar);
464 472
             } else {
465 473
                 topBar.show();
466 474
             }
467 475
         }
468
-        if (options.drawBehind.isTrue()) {
476
+        if (topBarOptions.drawBehind.isTrue()) {
469 477
             component.drawBehindTopBar();
470 478
         }
471
-        if (options.drawBehind.isFalse()) {
479
+        if (topBarOptions.drawBehind.isFalse()) {
472 480
             component.drawBelowTopBar(topBar);
473 481
         }
474
-        if (options.hideOnScroll.isTrue() && component instanceof IReactView) {
482
+        if (topBarOptions.hideOnScroll.isTrue() && component instanceof IReactView) {
475 483
             topBar.enableCollapse(((IReactView) component).getScrollEventListener());
476 484
         }
477
-        if (options.hideOnScroll.isFalse()) {
485
+        if (topBarOptions.hideOnScroll.isFalse()) {
478 486
             topBar.disableCollapse();
479 487
         }
480 488
     }

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

@@ -10,6 +10,7 @@ import android.widget.FrameLayout;
10 10
 import com.reactnativenavigation.parse.Options;
11 11
 import com.reactnativenavigation.presentation.OverlayManager;
12 12
 import com.reactnativenavigation.presentation.Presenter;
13
+import com.reactnativenavigation.presentation.RootPresenter;
13 14
 import com.reactnativenavigation.react.EventEmitter;
14 15
 import com.reactnativenavigation.utils.CommandListener;
15 16
 import com.reactnativenavigation.utils.CommandListenerAdapter;

+ 10
- 1
lib/android/app/src/main/java/com/reactnativenavigation/views/BottomTabs.java View File

@@ -4,11 +4,15 @@ import android.annotation.SuppressLint;
4 4
 import android.content.Context;
5 5
 import android.graphics.drawable.Drawable;
6 6
 import android.support.annotation.IntRange;
7
+import android.widget.LinearLayout;
7 8
 
8 9
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
9 10
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;
11
+import com.reactnativenavigation.parse.LayoutDirection;
10 12
 import com.reactnativenavigation.utils.CompatUtils;
11 13
 
14
+import static com.reactnativenavigation.utils.ViewUtils.findChildByClass;
15
+
12 16
 @SuppressLint("ViewConstructor")
13 17
 public class BottomTabs extends AHBottomNavigation {
14 18
     private boolean itemsCreationEnabled = true;
@@ -39,7 +43,7 @@ public class BottomTabs extends AHBottomNavigation {
39 43
 
40 44
     @Override
41 45
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
42
-
46
+        // NOOP - don't recreate views on size change
43 47
     }
44 48
 
45 49
     public void superCreateItems() {
@@ -75,4 +79,9 @@ public class BottomTabs extends AHBottomNavigation {
75 79
             refresh();
76 80
         }
77 81
     }
82
+
83
+    public void setLayoutDirection(LayoutDirection direction) {
84
+         LinearLayout tabsContainer = findChildByClass(this, LinearLayout.class);
85
+        if (tabsContainer != null) tabsContainer.setLayoutDirection(direction.get());
86
+    }
78 87
 }

+ 5
- 0
lib/android/app/src/main/java/com/reactnativenavigation/views/topbar/TopBar.java View File

@@ -26,6 +26,7 @@ import com.reactnativenavigation.anim.TopBarCollapseBehavior;
26 26
 import com.reactnativenavigation.interfaces.ScrollEventListener;
27 27
 import com.reactnativenavigation.parse.Alignment;
28 28
 import com.reactnativenavigation.parse.AnimationOptions;
29
+import com.reactnativenavigation.parse.LayoutDirection;
29 30
 import com.reactnativenavigation.parse.params.Colour;
30 31
 import com.reactnativenavigation.parse.params.Number;
31 32
 import com.reactnativenavigation.utils.CompatUtils;
@@ -331,4 +332,8 @@ public class TopBar extends AppBarLayout implements ScrollEventListener.ScrollAw
331 332
     public void setOverflowButtonColor(int color) {
332 333
         titleBar.setOverflowButtonColor(color);
333 334
     }
335
+
336
+    public void setLayoutDirection(LayoutDirection direction) {
337
+        titleBar.setLayoutDirection(direction.get());
338
+    }
334 339
 }

+ 1
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsPresenterTest.java View File

@@ -40,7 +40,7 @@ public class BottomTabsPresenterTest extends BaseTest {
40 40
     }
41 41
 
42 42
     @Test
43
-    public void mergeChildOptions_onlyDeclaredOptionsAreApplied() { // default options are not applies on merge
43
+    public void mergeChildOptions_onlyDeclaredOptionsAreApplied() { // default options are not applied on merge
44 44
         Options defaultOptions = new Options();
45 45
         defaultOptions.bottomTabsOptions.visible = new Bool(false);
46 46
         uut.setDefaultOptions(defaultOptions);

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

@@ -18,6 +18,7 @@ import com.reactnativenavigation.presentation.BottomTabPresenter;
18 18
 import com.reactnativenavigation.presentation.BottomTabsPresenter;
19 19
 import com.reactnativenavigation.presentation.Presenter;
20 20
 import com.reactnativenavigation.presentation.OverlayManager;
21
+import com.reactnativenavigation.presentation.RootPresenter;
21 22
 import com.reactnativenavigation.react.EventEmitter;
22 23
 import com.reactnativenavigation.utils.CommandListener;
23 24
 import com.reactnativenavigation.utils.CommandListenerAdapter;

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

@@ -5,19 +5,20 @@ import android.support.annotation.NonNull;
5 5
 import android.view.View;
6 6
 import android.widget.FrameLayout;
7 7
 
8
+import com.facebook.react.ReactInstanceManager;
8 9
 import com.reactnativenavigation.BaseTest;
9 10
 import com.reactnativenavigation.anim.NavigationAnimator;
10 11
 import com.reactnativenavigation.mocks.SimpleViewController;
11 12
 import com.reactnativenavigation.parse.AnimationOptions;
12 13
 import com.reactnativenavigation.parse.Options;
13 14
 import com.reactnativenavigation.parse.params.Bool;
15
+import com.reactnativenavigation.presentation.LayoutDirectionApplier;
16
+import com.reactnativenavigation.presentation.RootPresenter;
14 17
 import com.reactnativenavigation.utils.CommandListenerAdapter;
15 18
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
16 19
 import com.reactnativenavigation.viewcontrollers.ViewController;
17 20
 import com.reactnativenavigation.views.element.ElementTransitionManager;
18 21
 
19
-import com.facebook.react.ReactInstanceManager;
20
-
21 22
 import org.junit.Test;
22 23
 import org.mockito.ArgumentCaptor;
23 24
 import org.mockito.Mockito;
@@ -36,6 +37,7 @@ public class RootPresenterTest extends BaseTest {
36 37
     private FrameLayout rootContainer;
37 38
     private ViewController root;
38 39
     private NavigationAnimator animator;
40
+    private LayoutDirectionApplier layoutDirectionApplier;
39 41
     private Options defaultOptions;
40 42
     private ReactInstanceManager reactInstanceManager;
41 43
 
@@ -47,7 +49,8 @@ public class RootPresenterTest extends BaseTest {
47 49
         rootContainer = new FrameLayout(activity);
48 50
         root = new SimpleViewController(activity, new ChildControllersRegistry(), "child1", new Options());
49 51
         animator = spy(createAnimator(activity));
50
-        uut = new RootPresenter(animator);
52
+        layoutDirectionApplier = Mockito.mock(LayoutDirectionApplier.class);
53
+        uut = new RootPresenter(animator, layoutDirectionApplier);
51 54
         uut.setRootContainer(rootContainer);
52 55
         defaultOptions = new Options();
53 56
     }
@@ -120,6 +123,13 @@ public class RootPresenterTest extends BaseTest {
120 123
         verify(listener).onSuccess(spy.getId());
121 124
     }
122 125
 
126
+    @Test
127
+    public void setRoot_appliesLayoutDirection() {
128
+        CommandListenerAdapter listener = spy(new CommandListenerAdapter());
129
+        uut.setRoot(root, defaultOptions, listener, reactInstanceManager);
130
+        verify(layoutDirectionApplier).apply(root, defaultOptions, reactInstanceManager);
131
+    }
132
+
123 133
     @NonNull
124 134
     private NavigationAnimator createAnimator(Activity activity) {
125 135
         return new NavigationAnimator(activity, mock(ElementTransitionManager.class)) {

+ 1
- 0
playground/android/app/src/main/AndroidManifest.xml View File

@@ -10,6 +10,7 @@
10 10
         android:allowBackup="false"
11 11
         android:icon="@mipmap/ic_launcher"
12 12
         android:label="@string/app_name"
13
+        android:supportsRtl="true"
13 14
         android:theme="@style/AppTheme"
14 15
         android:usesCleartextTraffic="true"
15 16
         tools:ignore="GoogleAppIndexingWarning">

+ 2
- 1
playground/src/commons/Options.js View File

@@ -4,7 +4,8 @@ const Colors = require('./Colors');
4 4
 const setDefaultOptions = () => Navigation.setDefaultOptions({
5 5
   layout: {
6 6
     componentBackgroundColor: Colors.background,
7
-    orientation: ['portrait']
7
+    orientation: ['portrait'],
8
+    direction: 'locale'
8 9
   },
9 10
   bottomTabs: {
10 11
     titleDisplayMode: 'alwaysShow'