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.
Persian/賮丕乇爻蹖
Arabic
Push a component to navigator and __look at navigation__
_If works, back button should displayed at right either title._
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));
});
}
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 } });
<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: {
...
}
},
});
As you know back button icon should be flipped , in iOS works perfect by default but android needs some hacks.
{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>
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");
}
}
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);
}
TopBarButtonController.java
:edit
applyNavigationIcon
method
public void applyNavigationIcon(Toolbar toolbar, Integer direction) {
navigationIconResolver.resolve(button, direction, icon -> {
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);
}
});
}
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];
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: {
...
}
},
});
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:
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)
Most helpful comment
Workaround for iOS
1. Edit
RNNCommandsHelper.m
:2. Open __RNN__
Commands.js
file and modify setRoot method: _(Same as android workaround)_Now you can choose layout direction by passing _direction_ param as following :
3. Modify
RNNNavigationStackManager.m
: