Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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();
}
}
}
65 changes: 63 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 @@ -38,6 +36,7 @@ protected static View recycleView(View view) {
}

protected Screen mScreenView;
protected boolean mHasChildWithConfig = false;
private List<ScreenContainer> mChildScreenContainers = new ArrayList<>();

public ScreenFragment() {
Expand Down Expand Up @@ -66,6 +65,68 @@ public Screen getScreen() {
return mScreenView;
}

public void onContainerUpdate() {
mHasChildWithConfig = hasChildScreenWithConfig(getScreen());
markParentFragments(mHasChildWithConfig);
if (!mHasChildWithConfig) {
// 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 = ((Screen) parent).getContainer();
} else {
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 (topScreen.getFragment().mHasChildWithConfig) {
return true;
}
}
return false;
}

protected void markParentFragments(boolean hasChildWithConfig) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is correct. If there is one childrent with config and one without, then when the second one updates, it will send information to the parent that there is no config (but the first childrent still has config).

I think that instead this method should actually check flags with all its children (not recursively) and then if the new value for the flag is different than the previous one also notify its parent

ViewParent parent = getScreen().getContainer();
while (parent != null) {
if (parent instanceof Screen) {
((Screen) parent).getFragment().mHasChildWithConfig = hasChildWithConfig;
Copy link
Member

Choose a reason for hiding this comment

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

If this does not result in mHasChildWithConfig change there is no point in updating other parents up the view hierarchy (there will be no updates as well) – we can break the loop if this occurs

parent = ((Screen) parent).getContainer();
} else {
parent = parent.getParent();
}
}
}

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
Loading