Skip to content

Commit a9e8317

Browse files
committed
feat: android orientation management (#679)
SC will stand for ScreenContainer. Added orientation management on Android. On every update of native-stack or navigators with using SC we check if there is orientation change needed. We do it by checking the top Screen and, if there is no child HeaderConfig (which has the priority) and there is a HeaderConfig above the Screen, changing the orientation according to it. If there are no child SCs in the Screen in onContainerUpdate even though the Screen has a child SC, it means that the child has not fired onAttachedToWindow, where the registration of child SC happens. It is not a problem, because then the correct update of orientation will be fired in onAttachedToWindow of the child Screen HeaderConfig, if it exists.
1 parent 804e603 commit a9e8317

File tree

11 files changed

+189
-39
lines changed

11 files changed

+189
-39
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,21 @@ Customize the weight of the font to be used for the large title.
272272

273273
Boolean that allows for disabling drop shadow under navigation header when the edge of any scrollable content reaches the matching edge of the navigation bar.
274274

275+
#### `screenOrientation`
276+
277+
Sets the current screen's available orientations and forces rotation if current orientation is not included. Possible values:
278+
279+
- `default` - on iOS, it resolves to [UIInterfaceOrientationMaskAllButUpsideDown](https://developer.apple.com/documentation/uikit/uiinterfaceorientationmask/uiinterfaceorientationmaskallbutupsidedown?language=objc). On Android, this lets the system decide the best orientation.
280+
- `all`
281+
- `portrait`
282+
- `portrait_up`
283+
- `portrait_down`
284+
- `landscape`
285+
- `landscape_left`
286+
- `landscape_right`
287+
288+
Defaults to `default`.
289+
275290
#### `statusBarAnimation` (iOS only)
276291

277292
Sets the status bar animation (similar to the `StatusBar` component). Requires enabling (or deleting) `View controller-based status bar appearance` in your Info.plist file. Defaults to `fade`.

TestsExample/src/Test42.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44
import {ScrollView, StyleSheet, View, Button, Text} from 'react-native';
55
import {createNativeStackNavigator} from 'react-native-screens/native-stack';
66
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
7+
import {createStackNavigator} from '@react-navigation/stack';
78

89
const Stack = createNativeStackNavigator();
910

@@ -22,8 +23,8 @@ export default function NativeNavigation() {
2223
}}
2324
/>
2425
<Stack.Screen
25-
name="TabNavigator"
26-
component={TabNavigator}
26+
name="NestedNavigator"
27+
component={NestedNavigator}
2728
options={{
2829
screenOrientation: 'landscape',
2930
}}
@@ -33,13 +34,14 @@ export default function NativeNavigation() {
3334
);
3435
}
3536

37+
// change to createStackNavigator to test with stack in the middle
3638
const Tab = createBottomTabNavigator();
3739

38-
const TabNavigator = (props) => (
40+
const NestedNavigator = (props) => (
3941
<Tab.Navigator screensEnabled={true}>
40-
<Tab.Screen name="Tab1" component={Home} />
41-
<Tab.Screen name="Tab2" component={Inner} />
42-
<Tab.Screen name="Tab3" component={Home} />
42+
<Tab.Screen name="Screen1" component={Home} />
43+
<Tab.Screen name="Screen2" component={Inner} />
44+
<Tab.Screen name="Screen3" component={Home} />
4345
</Tab.Navigator>
4446
);
4547

@@ -63,9 +65,15 @@ function Home({navigation}) {
6365
>
6466
<View style={styles.leftTop} />
6567
<Button
66-
title="TabNavigator"
68+
title="NestedNavigator"
6769
onPress={() => {
68-
navigation.push('TabNavigator');
70+
navigation.push('NestedNavigator');
71+
}}
72+
/>
73+
<Button
74+
title="Screen2"
75+
onPress={() => {
76+
navigation.navigate('Screen2');
6977
}}
7078
/>
7179
<Button

android/src/main/java/com/swmansion/rnscreens/Screen.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ protected void onAttachedToWindow() {
150150
}
151151
}
152152

153+
protected ScreenStackHeaderConfig getHeaderConfig() {
154+
View child = getChildAt(0);
155+
if (child instanceof ScreenStackHeaderConfig) {
156+
return (ScreenStackHeaderConfig) child;
157+
}
158+
return null;
159+
}
160+
153161
/**
154162
* While transitioning this property allows to optimize rendering behavior on Android and provide
155163
* a correct blending options for the animated screen. It is turned on automatically by the container

android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ protected void removeScreenAt(int index) {
136136
}
137137

138138
protected void removeAllScreens() {
139-
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
140-
mScreenFragments.get(i).getScreen().setContainer(null);
139+
for (ScreenFragment screenFragment: mScreenFragments) {
140+
screenFragment.getScreen().setContainer(null);
141141
}
142142
mScreenFragments.clear();
143143
markUpdated();
@@ -151,6 +151,15 @@ protected Screen getScreenAt(int index) {
151151
return mScreenFragments.get(index).getScreen();
152152
}
153153

154+
public @Nullable Screen getTopScreen() {
155+
for (ScreenFragment screenFragment: mScreenFragments) {
156+
if (getActivityState(screenFragment) == Screen.ActivityState.ON_TOP) {
157+
return screenFragment.getScreen();
158+
}
159+
}
160+
return null;
161+
}
162+
154163
private void setFragmentManager(FragmentManager fm) {
155164
mFragmentManager = fm;
156165
updateIfNeeded();
@@ -328,13 +337,13 @@ private final void onUpdate() {
328337
mFragmentManager.executePendingTransactions();
329338

330339
performUpdate();
340+
notifyContainerUpdate();
331341
}
332342

333343
protected void performUpdate() {
334344
// detach screens that are no longer active
335345
Set<Fragment> orphaned = new HashSet<>(mFragmentManager.getFragments());
336-
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
337-
ScreenFragment screenFragment = mScreenFragments.get(i);
346+
for (ScreenFragment screenFragment: mScreenFragments) {
338347
if (getActivityState(screenFragment) == Screen.ActivityState.INACTIVE && screenFragment.isAdded()) {
339348
detachScreen(screenFragment);
340349
}
@@ -353,18 +362,16 @@ protected void performUpdate() {
353362

354363
boolean transitioning = true;
355364

356-
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
357-
ScreenFragment screenFragment = mScreenFragments.get(i);
358-
if (getActivityState(screenFragment) == Screen.ActivityState.ON_TOP) {
359-
// if there is an "onTop" screen it means the transition has ended
360-
transitioning = false;
361-
}
365+
366+
Screen topScreen = getTopScreen();
367+
if (topScreen != null) {
368+
// if there is an "onTop" screen it means the transition has ended
369+
transitioning = false;
362370
}
363371

364372
// attach newly activated screens
365373
boolean addedBefore = false;
366-
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
367-
ScreenFragment screenFragment = mScreenFragments.get(i);
374+
for (ScreenFragment screenFragment: mScreenFragments) {
368375
Screen.ActivityState activityState = getActivityState(screenFragment);
369376
if (activityState != Screen.ActivityState.INACTIVE && !screenFragment.isAdded()) {
370377
addedBefore = true;
@@ -377,4 +384,11 @@ protected void performUpdate() {
377384

378385
tryCommitTransaction();
379386
}
387+
388+
protected void notifyContainerUpdate() {
389+
Screen topScreen = getTopScreen();
390+
if (topScreen != null) {
391+
topScreen.getFragment().onContainerUpdate();
392+
}
393+
}
380394
}

android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package com.swmansion.rnscreens;
22

33
import android.annotation.SuppressLint;
4-
import android.content.Context;
54
import android.os.Bundle;
65
import android.view.LayoutInflater;
76
import android.view.View;
87
import android.view.ViewGroup;
98
import android.view.ViewParent;
10-
import android.view.inputmethod.InputMethodManager;
119
import android.widget.FrameLayout;
1210

1311
import com.facebook.react.bridge.ReactContext;
@@ -66,6 +64,52 @@ public Screen getScreen() {
6664
return mScreenView;
6765
}
6866

67+
public void onContainerUpdate() {
68+
if (!hasChildScreenWithConfig(getScreen())) {
69+
// if there is no child with config, we look for a parent with config to set the orientation
70+
ScreenStackHeaderConfig config = findHeaderConfig();
71+
if (config != null && config.getScreenFragment().getActivity() != null) {
72+
config.getScreenFragment().getActivity().setRequestedOrientation(config.getScreenOrientation());
73+
}
74+
}
75+
}
76+
77+
private @Nullable ScreenStackHeaderConfig findHeaderConfig() {
78+
ViewParent parent = getScreen().getContainer();
79+
while (parent != null) {
80+
if (parent instanceof Screen) {
81+
ScreenStackHeaderConfig headerConfig = ((Screen) parent).getHeaderConfig();
82+
if (headerConfig != null) {
83+
return headerConfig;
84+
}
85+
}
86+
parent = parent.getParent();
87+
}
88+
return null;
89+
}
90+
91+
protected boolean hasChildScreenWithConfig(Screen screen) {
92+
if (screen == null) {
93+
return false;
94+
}
95+
for (ScreenContainer sc : screen.getFragment().getChildScreenContainers()) {
96+
// we check only the top screen for header config
97+
Screen topScreen = sc.getTopScreen();
98+
ScreenStackHeaderConfig headerConfig = topScreen != null ? topScreen.getHeaderConfig(): null;
99+
if (headerConfig != null) {
100+
return true;
101+
}
102+
if (hasChildScreenWithConfig(topScreen)) {
103+
return true;
104+
}
105+
}
106+
return false;
107+
}
108+
109+
public List<ScreenContainer> getChildScreenContainers() {
110+
return mChildScreenContainers;
111+
}
112+
69113
protected void dispatchOnWillAppear() {
70114
((ReactContext) mScreenView.getContext())
71115
.getNativeModule(UIManagerModule.class)

android/src/main/java/com/swmansion/rnscreens/ScreenStack.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import android.content.Context;
44
import android.view.View;
55

6+
import androidx.annotation.Nullable;
67
import androidx.fragment.app.Fragment;
78
import androidx.fragment.app.FragmentManager;
89
import androidx.fragment.app.FragmentTransaction;
@@ -54,7 +55,8 @@ public void dismiss(ScreenStackFragment screenFragment) {
5455
markUpdated();
5556
}
5657

57-
public Screen getTopScreen() {
58+
@Override
59+
public @Nullable Screen getTopScreen() {
5860
return mTopScreen != null ? mTopScreen.getScreen() : null;
5961
}
6062

@@ -261,9 +263,12 @@ public void run() {
261263
if (mTopScreen != null) {
262264
setupBackHandlerIfNeeded(mTopScreen);
263265
}
266+
}
264267

268+
@Override
269+
protected void notifyContainerUpdate() {
265270
for (ScreenStackFragment screen : mStack) {
266-
screen.onStackUpdate();
271+
screen.onContainerUpdate();
267272
}
268273
}
269274

android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ public void setToolbarTranslucent(boolean translucent) {
109109
}
110110
}
111111

112-
public void onStackUpdate() {
113-
View child = mScreenView.getChildAt(0);
114-
if (child instanceof ScreenStackHeaderConfig) {
115-
((ScreenStackHeaderConfig) child).onUpdate();
112+
@Override
113+
public void onContainerUpdate() {
114+
ScreenStackHeaderConfig headerConfig = getScreen().getHeaderConfig();
115+
if (headerConfig != null) {
116+
headerConfig.onUpdate();
116117
}
117118
}
118119

android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.swmansion.rnscreens;
22

33
import android.content.Context;
4+
import android.content.pm.ActivityInfo;
45
import android.graphics.PorterDuff;
56
import android.graphics.drawable.Drawable;
67
import android.os.Build;
@@ -13,6 +14,7 @@
1314
import android.widget.ImageView;
1415
import android.widget.TextView;
1516

17+
import androidx.annotation.Nullable;
1618
import androidx.appcompat.app.ActionBar;
1719
import androidx.appcompat.app.AppCompatActivity;
1820
import androidx.appcompat.widget.Toolbar;
@@ -42,6 +44,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
4244
private boolean mIsTranslucent;
4345
private int mTintColor;
4446
private final Toolbar mToolbar;
47+
private int mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
4548

4649
private boolean mIsAttachedToWindow = false;
4750

@@ -136,7 +139,7 @@ private ScreenStack getScreenStack() {
136139
return null;
137140
}
138141

139-
private ScreenStackFragment getScreenFragment() {
142+
protected @Nullable ScreenStackFragment getScreenFragment() {
140143
ViewParent screen = getParent();
141144
if (screen instanceof Screen) {
142145
Fragment fragment = ((Screen) screen).getFragment();
@@ -169,6 +172,12 @@ public void onUpdate() {
169172
}
170173
}
171174

175+
// orientation
176+
if (getScreenFragment() == null || !getScreenFragment().hasChildScreenWithConfig(getScreen())) {
177+
// we check if there is no child that provides config, since then we shouldn't change orientation here
178+
activity.setRequestedOrientation(mScreenOrientation);
179+
}
180+
172181
if (mIsHidden) {
173182
if (mToolbar.getParent() != null) {
174183
getScreenFragment().removeToolbar();
@@ -345,6 +354,10 @@ private TextView getTitleTextView() {
345354
return null;
346355
}
347356

357+
public int getScreenOrientation() {
358+
return mScreenOrientation;
359+
}
360+
348361
public void setTitle(String title) {
349362
mTitle = title;
350363
}
@@ -392,4 +405,38 @@ public void setTranslucent(boolean translucent) {
392405
public void setDirection(String direction) {
393406
mDirection = direction;
394407
}
408+
409+
public void setScreenOrientation(String screenOrientation) {
410+
if (screenOrientation == null) {
411+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
412+
return;
413+
}
414+
415+
switch (screenOrientation) {
416+
case "all":
417+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
418+
break;
419+
case "portrait":
420+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
421+
break;
422+
case "portrait_up":
423+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
424+
break;
425+
case "portrait_down":
426+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
427+
break;
428+
case "landscape":
429+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
430+
break;
431+
case "landscape_left":
432+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
433+
break;
434+
case "landscape_right":
435+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
436+
break;
437+
default:
438+
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
439+
break;
440+
}
441+
}
395442
}

android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ public void setDirection(ScreenStackHeaderConfig config, String direction) {
134134
config.setDirection(direction);
135135
}
136136

137+
@ReactProp(name = "screenOrientation")
138+
public void setScreenOrientation(ScreenStackHeaderConfig config, String screenOrientation) {
139+
config.setScreenOrientation(screenOrientation);
140+
}
137141

138142
// RCT_EXPORT_VIEW_PROPERTY(backTitle, NSString)
139143
// RCT_EXPORT_VIEW_PROPERTY(backTitleFontFamily, NSString)

0 commit comments

Comments
 (0)