React-native-navigation: [V2] RTL Layout Support

Created on 3 Jul 2018  路  5Comments  路  Source: wix/react-native-navigation

Issue Description

In many cases our applications need to support __RTL__ languages, __React__ allows to _RTL_ via I18nManager API, but __RNN__ does not support any options to deal with layout directions ( Only loves _LTR_ 馃槩 ).

Android Platform:
In RNN v1 when system language was a RTL language (like: Persian or Arabic) or simply call I18nManager.forceRTL(true), Everything goes fine, back button shown in right.
But in RNN v2 Nothing, always left and always LTR, no way to done it.

Steps to Reproduce / Code Snippets / Screenshots

  1. Change system language to > Persian/賮丕乇爻蹖 Arabic
  2. Restart _RNN_ App
  3. Push a component to navigator and __look at navigation__

    _If works, back button should displayed at right either title._

#### UPDATE: I described workarounds for both _Android_ and _iOS_ platforms in comments.

Environment

  • React Native Navigation version: 2.0.2394
  • React Native version: 0.55.4
  • Platform(s) (iOS, Android, or both?): Both
  • Device info (Simulator/Device? OS version? Debug/Release?): Simulator
Android iOS looking for contributors v2

Most helpful comment

Workaround for iOS

1. Edit RNNCommandsHelper.m:

-(void) setRoot:(NSDictionary*)layout completion:(RNNTransitionCompletionBlock)completion {

    if (@available(iOS 9, *)) {
        if ([layout[@"direction"] 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];
        }
    }

    [self assertReady];

2. Open __RNN__ Commands.js file and modify setRoot method: _(Same as android workaround)_

pass direction param down to native module

const direction = simpleApi.direction ? simpleApi.direction : 'ltr';
        const commandId = this.uniqueIdProvider.generate('setRoot');
        const result = this.nativeCommandsSender.setRoot(commandId, { direction, root, modals, overlays });
        this.commandsObserver.notify('setRoot', { commandId, layout: { direction, root, modals, overlays } });

Now you can choose layout direction by passing _direction_ param as following :

Navigation.setRoot({
+   direction: 'rtl',
      root: {
        ...
      }
    },
  });

3. Modify RNNNavigationStackManager.m:

Fix back gesture issue

...
+ #import <React/RCTI18nUtil.h>


typedef void (^RNNAnimationBlock)(void);

@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;
+   }

All 5 comments

Workaround for android

RTL Layout

1. Modify RNN NavigationModule.java

Add newLayoutFactory method:

@NonNull
    private LayoutFactory newLayoutFactory(String direction) {
        Activity appActivity = activity();
        appActivity.getWindow().getDecorView().setLayoutDirection(direction.equals("rtl") ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);

        I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
        if(direction.equals("rtl")) {
            sharedI18nUtilInstance.allowRTL(getReactApplicationContext(), true);
            sharedI18nUtilInstance.forceRTL(getReactApplicationContext(), true);
        } else {
            sharedI18nUtilInstance.allowRTL(getReactApplicationContext(), false);
            sharedI18nUtilInstance.forceRTL(getReactApplicationContext(), false);
        }

        return new LayoutFactory(appActivity,
                navigator().getChildRegistry(),
                reactInstanceManager,
                eventEmitter,
                externalComponentCreator(),
                navigator().getDefaultOptions()
        );
    }

... and edit setRoot method as following:

@ReactMethod
    public void setRoot(String commandId, ReadableMap rawLayoutTree, Promise promise) {
        final LayoutNode layoutTree = LayoutNodeParser.parse(JSONParser.parse(rawLayoutTree).optJSONObject("root"));
        handle(() -> {
            final ViewController viewController = newLayoutFactory(rawLayoutTree.getString("direction")).create(layoutTree);
            navigator().setRoot(viewController, new NativeCommandListener(commandId, promise, eventEmitter, now));
        });
    }

2. Open __RNN__ Commands.js file and modify setRoot method:

pass direction param down to native module

const direction = simpleApi.direction ? simpleApi.direction : 'ltr';
        const commandId = this.uniqueIdProvider.generate('setRoot');
        const result = this.nativeCommandsSender.setRoot(commandId, { direction, root, modals, overlays });
        this.commandsObserver.notify('setRoot', { commandId, layout: { direction, root, modals, overlays } });

3. Don't forget to add supportsRTL to your AndroidManifest:

<application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:allowBackup="false"
+     android:supportsRtl="true"
      ...

Now you can choose layout direction by passing _direction_ param as following :

Navigation.setRoot({
+   direction: 'rtl',
      root: {
        ...
      }
    },
  });

Fix back button icon

As you know back button icon should be flipped , in iOS works perfect by default but android needs some hacks.

4. Add new drawable in {RNN}/res/drawable and name it ic_arrow_back_black_rtl_24dp.xml fill it with following content:

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

5. Modify NavigationIconResolver.java :

add a direction property and choose right drawable based on direction

public void resolve(Button button, Integer direction, Task<Drawable> onSuccess) {
        if (button.icon.hasValue()) {
            imageLoader.loadIcon(context, button.icon.get(), new ImageLoadingListenerAdapter() {
                @Override
                public void onComplete(@NonNull Drawable icon) {
                    onSuccess.run(icon);
                }

                @Override
                public void onError(Throwable error) {
                    throw new RuntimeException(error);
                }
            });
        } else if (Constants.BACK_BUTTON_ID.equals(button.id)) {
            onSuccess.run(ContextCompat.getDrawable(context, direction == 0 ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_black_rtl_24dp));
        } else {
            throw new RuntimeException("Left button needs to have an icon");
        }
    }

6. Modify TitleBar.java :

to find current layout direction and pass it down

private void setLeftButton(final Button button) {
        TopBarButtonController controller = createButtonController(button);
        leftButtonController = controller;
        Integer direction = reactViewController.getActivity().getWindow().getDecorView().getLayoutDirection();
        controller.applyNavigationIcon(this, direction);
    }

7. Modify TopBarButtonController.java :

edit applyNavigationIcon method

 public void applyNavigationIcon(Toolbar toolbar, Integer direction) {
        navigationIconResolver.resolve(button, direction, icon -> {

Fix title text alignment

8. Modify TitleBar.java :

private void alignTextView(Alignment alignment, TextView view) {
        view.post(() -> {
            Integer direction = view.getParent().getLayoutDirection();
            if (alignment == Alignment.Center) {
                view.setX((getWidth() - view.getWidth()) / 2);
            } else if (leftButtonController != null) {
                view.setX(direction == 1 ? (getWidth() - view.getWidth()) - getContentInsetStartWithNavigation() : getContentInsetStartWithNavigation());
            } else {
                Float xOffset = UiUtils.dpToPx(getContext(), 16);
                view.setX(direction == 1 ? (getWidth() - view.getWidth()) - xOffset : xOffset);
            }
        });
    }

Workaround for iOS

1. Edit RNNCommandsHelper.m:

-(void) setRoot:(NSDictionary*)layout completion:(RNNTransitionCompletionBlock)completion {

    if (@available(iOS 9, *)) {
        if ([layout[@"direction"] 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];
        }
    }

    [self assertReady];

2. Open __RNN__ Commands.js file and modify setRoot method: _(Same as android workaround)_

pass direction param down to native module

const direction = simpleApi.direction ? simpleApi.direction : 'ltr';
        const commandId = this.uniqueIdProvider.generate('setRoot');
        const result = this.nativeCommandsSender.setRoot(commandId, { direction, root, modals, overlays });
        this.commandsObserver.notify('setRoot', { commandId, layout: { direction, root, modals, overlays } });

Now you can choose layout direction by passing _direction_ param as following :

Navigation.setRoot({
+   direction: 'rtl',
      root: {
        ...
      }
    },
  });

3. Modify RNNNavigationStackManager.m:

Fix back gesture issue

...
+ #import <React/RCTI18nUtil.h>


typedef void (^RNNAnimationBlock)(void);

@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;
+   }

great tricks 馃挴
looking forward for the official support.

in ios:

import

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    NSURL *jsCodeLocation;
    [[RCTI18nUtil sharedInstance] allowRTL:YES];
    [[RCTI18nUtil sharedInstance] forceRTL:YES];

Thanks, but i'm facing back gesture issue again (in IOS), stack layout direction is fine but swipe action is reverse.
Dragging should start from left side, but the stack start pushing out from right side (wrong swipe direction)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

yayanartha picture yayanartha  路  3Comments

henrikra picture henrikra  路  3Comments

edcs picture edcs  路  3Comments

zagoa picture zagoa  路  3Comments

bdrobinson picture bdrobinson  路  3Comments