Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,21 @@ Customize the weight of the font to be used for the large title.

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.

#### `screenOrientation`

Sets the current screen's available orientations and forces rotation if current orientation is not included. Possible values:

- `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.
- `all`
- `portrait`
- `portrait_up`
- `portrait_down`
- `landscape`
- `landscape_left`
- `landscape_right`

Defaults to `default`.

#### `statusBarAnimation` (iOS only)

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`.
Expand Down
24 changes: 16 additions & 8 deletions TestsExample/src/Test42.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import {ScrollView, StyleSheet, View, Button, Text} from 'react-native';
import {createNativeStackNavigator} from 'react-native-screens/native-stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {createStackNavigator} from '@react-navigation/stack';

const Stack = createNativeStackNavigator();

Expand All @@ -22,8 +23,8 @@ export default function NativeNavigation() {
}}
/>
<Stack.Screen
name="TabNavigator"
component={TabNavigator}
name="NestedNavigator"
component={NestedNavigator}
options={{
screenOrientation: 'landscape',
}}
Expand All @@ -33,13 +34,14 @@ export default function NativeNavigation() {
);
}

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

const TabNavigator = (props) => (
const NestedNavigator = (props) => (
<Tab.Navigator screensEnabled={true}>
<Tab.Screen name="Tab1" component={Home} />
<Tab.Screen name="Tab2" component={Inner} />
<Tab.Screen name="Tab3" component={Home} />
<Tab.Screen name="Screen1" component={Home} />
<Tab.Screen name="Screen2" component={Inner} />
<Tab.Screen name="Screen3" component={Home} />
</Tab.Navigator>
);

Expand All @@ -63,9 +65,15 @@ function Home({navigation}) {
>
<View style={styles.leftTop} />
<Button
title="TabNavigator"
title="NestedNavigator"
onPress={() => {
navigation.push('TabNavigator');
navigation.push('NestedNavigator');
}}
/>
<Button
title="Screen2"
onPress={() => {
navigation.navigate('Screen2');
}}
/>
<Button
Expand Down
8 changes: 8 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/Screen.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ protected void onAttachedToWindow() {
}
}

protected ScreenStackHeaderConfig getHeaderConfig() {
View child = getChildAt(0);
if (child instanceof ScreenStackHeaderConfig) {
return (ScreenStackHeaderConfig) child;
}
return null;
}

/**
* While transitioning this property allows to optimize rendering behavior on Android and provide
* a correct blending options for the animated screen. It is turned on automatically by the container
Expand Down
38 changes: 26 additions & 12 deletions android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ protected void removeScreenAt(int index) {
}

protected void removeAllScreens() {
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
mScreenFragments.get(i).getScreen().setContainer(null);
for (ScreenFragment screenFragment: mScreenFragments) {
screenFragment.getScreen().setContainer(null);
}
mScreenFragments.clear();
markUpdated();
Expand All @@ -151,6 +151,15 @@ protected Screen getScreenAt(int index) {
return mScreenFragments.get(index).getScreen();
}

public @Nullable Screen getTopScreen() {
for (ScreenFragment screenFragment: mScreenFragments) {
if (getActivityState(screenFragment) == Screen.ActivityState.ON_TOP) {
return screenFragment.getScreen();
}
}
return null;
}

private void setFragmentManager(FragmentManager fm) {
mFragmentManager = fm;
updateIfNeeded();
Expand Down Expand Up @@ -328,13 +337,13 @@ private final void onUpdate() {
mFragmentManager.executePendingTransactions();

performUpdate();
notifyContainerUpdate();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be called directly from perfomUpdate? Are there any other ways performUpdate method is called when we don't want notify to be triggered? If not why?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think not. I added this here since we call it both in ScreenContainer and ScreenStack this way and if it is in perfomUpdate, then we need to use this method in both classes. I don't have a strong opinion on this.

}

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

boolean transitioning = true;

for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
ScreenFragment screenFragment = mScreenFragments.get(i);
if (getActivityState(screenFragment) == Screen.ActivityState.ON_TOP) {
// if there is an "onTop" screen it means the transition has ended
transitioning = false;
}

Screen topScreen = getTopScreen();
if (topScreen != null) {
// if there is an "onTop" screen it means the transition has ended
transitioning = false;
}

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

tryCommitTransaction();
}

protected void notifyContainerUpdate() {
Screen topScreen = getTopScreen();
if (topScreen != null) {
topScreen.getFragment().onContainerUpdate();
}
}
}
48 changes: 46 additions & 2 deletions android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.swmansion.rnscreens;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;

import com.facebook.react.bridge.ReactContext;
Expand Down Expand Up @@ -66,6 +64,52 @@ public Screen getScreen() {
return mScreenView;
}

public void onContainerUpdate() {
if (!hasChildScreenWithConfig(getScreen())) {
// if there is no child with config, we look for a parent with config to set the orientation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still a bit confused by this. Why we check if children have config while findHeaderConfig searches for the config in current container or in its parents? If it isn't trivial to explain maybe we can discuss over call and figure out best way to describe it here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have a structure of ScreenStack -> ScreenContainer -> ScreenStack. We do this check in case the update of the container was triggered after setting the orientation in the child stack, which would result in setting the orientation of the parent ScreenStack even though the child ScreenStack should have priority. At the same time, we want to have an option to change the orientation on every update of container, because the middle ScreenContainer can have children that are not ScreenStacks, so after navigating from a child that is a ScreenStack to a child that is not, we want the orientation to be taken and applied from the parent ScreenStack.

Is this explanation clear enough?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it is probably good to change the code to only look for the parent Screen with config if there is no child with config to avoid traversing the hierarchy when not needed.

ScreenStackHeaderConfig config = findHeaderConfig();
if (config != null && config.getScreenFragment().getActivity() != null) {
config.getScreenFragment().getActivity().setRequestedOrientation(config.getScreenOrientation());
}
}
}

private @Nullable ScreenStackHeaderConfig findHeaderConfig() {
ViewParent parent = getScreen().getContainer();
while (parent != null) {
if (parent instanceof Screen) {
ScreenStackHeaderConfig headerConfig = ((Screen) parent).getHeaderConfig();
if (headerConfig != null) {
return headerConfig;
}
}
parent = parent.getParent();
}
return null;
}

protected boolean hasChildScreenWithConfig(Screen screen) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it'd be worth keeping track of whether there are children with config upon container updates. It seem like this parameter can change when we add new children or when the "top screen changes". Maybe we could add some bottom-up notification mechanism that'd be triggered when such an even occur that'd allow to keep the up-to-date information whether there are children with config instead of doing this traversal which potentially runs on every level in screen container hierarchy and runs the same code for the same container instances multiple times.

if (screen == null) {
return false;
}
for (ScreenContainer sc : screen.getFragment().getChildScreenContainers()) {
// we check only the top screen for header config
Screen topScreen = sc.getTopScreen();
ScreenStackHeaderConfig headerConfig = topScreen != null ? topScreen.getHeaderConfig(): null;
if (headerConfig != null) {
return true;
}
if (hasChildScreenWithConfig(topScreen)) {
return true;
}
}
return false;
}

public List<ScreenContainer> getChildScreenContainers() {
return mChildScreenContainers;
}

protected void dispatchOnWillAppear() {
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.content.Context;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
Expand Down Expand Up @@ -54,7 +55,8 @@ public void dismiss(ScreenStackFragment screenFragment) {
markUpdated();
}

public Screen getTopScreen() {
@Override
public @Nullable Screen getTopScreen() {
return mTopScreen != null ? mTopScreen.getScreen() : null;
}

Expand Down Expand Up @@ -261,9 +263,12 @@ public void run() {
if (mTopScreen != null) {
setupBackHandlerIfNeeded(mTopScreen);
}
}

@Override
protected void notifyContainerUpdate() {
for (ScreenStackFragment screen : mStack) {
screen.onStackUpdate();
screen.onContainerUpdate();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,11 @@ public void setToolbarTranslucent(boolean translucent) {
}
}

public void onStackUpdate() {
View child = mScreenView.getChildAt(0);
if (child instanceof ScreenStackHeaderConfig) {
((ScreenStackHeaderConfig) child).onUpdate();
@Override
public void onContainerUpdate() {
ScreenStackHeaderConfig headerConfig = getScreen().getHeaderConfig();
if (headerConfig != null) {
headerConfig.onUpdate();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swmansion.rnscreens;

import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
Expand All @@ -13,6 +14,7 @@
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
Expand Down Expand Up @@ -42,6 +44,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
private boolean mIsTranslucent;
private int mTintColor;
private final Toolbar mToolbar;
private int mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;

private boolean mIsAttachedToWindow = false;

Expand Down Expand Up @@ -136,7 +139,7 @@ private ScreenStack getScreenStack() {
return null;
}

private ScreenStackFragment getScreenFragment() {
protected @Nullable ScreenStackFragment getScreenFragment() {
ViewParent screen = getParent();
if (screen instanceof Screen) {
Fragment fragment = ((Screen) screen).getFragment();
Expand Down Expand Up @@ -169,6 +172,12 @@ public void onUpdate() {
}
}

// orientation
if (getScreenFragment() == null || !getScreenFragment().hasChildScreenWithConfig(getScreen())) {
// we check if there is no child that provides config, since then we shouldn't change orientation here
activity.setRequestedOrientation(mScreenOrientation);
}

if (mIsHidden) {
if (mToolbar.getParent() != null) {
getScreenFragment().removeToolbar();
Expand Down Expand Up @@ -345,6 +354,10 @@ private TextView getTitleTextView() {
return null;
}

public int getScreenOrientation() {
return mScreenOrientation;
}

public void setTitle(String title) {
mTitle = title;
}
Expand Down Expand Up @@ -392,4 +405,38 @@ public void setTranslucent(boolean translucent) {
public void setDirection(String direction) {
mDirection = direction;
}

public void setScreenOrientation(String screenOrientation) {
if (screenOrientation == null) {
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
return;
}

switch (screenOrientation) {
case "all":
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
break;
case "portrait":
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
break;
case "portrait_up":
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
break;
case "portrait_down":
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
break;
case "landscape":
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
break;
case "landscape_left":
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
break;
case "landscape_right":
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
break;
default:
mScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
break;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ public void setDirection(ScreenStackHeaderConfig config, String direction) {
config.setDirection(direction);
}

@ReactProp(name = "screenOrientation")
public void setScreenOrientation(ScreenStackHeaderConfig config, String screenOrientation) {
config.setScreenOrientation(screenOrientation);
}

// RCT_EXPORT_VIEW_PROPERTY(backTitle, NSString)
// RCT_EXPORT_VIEW_PROPERTY(backTitleFontFamily, NSString)
Expand Down
Loading