Skip to content
Draft
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
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,9 @@ repositories {

dependencies {
implementation 'com.facebook.react:react-native:+'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.fragment:fragment-ktx:1.8.9'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.12.0'
implementation "androidx.core:core-ktx:1.8.0"
Expand Down
62 changes: 43 additions & 19 deletions android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.core.view.WindowInsetsCompat
import com.facebook.react.modules.core.ReactChoreographer
import com.facebook.react.uimanager.ThemedReactContext
import com.swmansion.rnscreens.utils.InsetsCompat
import com.swmansion.rnscreens.utils.RNSLog
import com.swmansion.rnscreens.utils.resolveInsetsOrZero
import kotlin.math.max

Expand All @@ -25,6 +26,10 @@ open class CustomToolbar(
context: Context,
val config: ScreenStackHeaderConfig,
) : Toolbar(context) {
init {
fitsSystemWindows = true
}

// Switch this flag to enable/disable display cutout avoidance.
// Currently this is controlled by isTopInsetEnabled prop.
private val shouldAvoidDisplayCutout
Expand All @@ -34,6 +39,7 @@ open class CustomToolbar(
get() = config.isTopInsetEnabled

private var lastInsets = InsetsCompat.NONE
private var laidOutAfterInsetsDispatch = false

private var isForceShadowStateUpdateOnLayoutRequested = false

Expand All @@ -52,6 +58,21 @@ open class CustomToolbar(
}
}

private fun requestLayoutInCurrentLoop() {
@Suppress("SENSELESS_COMPARISON") // mLayoutCallback can be null here since this method can be called in init
if (!isLayoutEnqueued && layoutCallback != null) {
isLayoutEnqueued = true
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
// looper loop instead of enqueueing the update in the next loop causing a one frame delay.
ReactChoreographer
.getInstance()
.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
layoutCallback,
)
}
}

override fun requestLayout() {
super.requestLayout()
val softInputMode =
Expand All @@ -66,23 +87,13 @@ open class CustomToolbar(
// the position of each subview, even if Yoga has correctly set their width and height).
// This is mostly the issue, when windowSoftInputMode is set to adjustPan in AndroidManifest.
// Thus, we're manually calling the layout **after** the current layout.
@Suppress("SENSELESS_COMPARISON") // mLayoutCallback can be null here since this method can be called in init
if (!isLayoutEnqueued && layoutCallback != null) {
isLayoutEnqueued = true
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
// looper loop instead of enqueueing the update in the next loop causing a one frame delay.
ReactChoreographer
.getInstance()
.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
layoutCallback,
)
}
requestLayoutInCurrentLoop()
}
}

override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets? {
val unhandledInsets = super.onApplyWindowInsets(insets)
RNSLog.d("Toolbar", "onApplyWindowInsets $unhandledInsets")

// There are few UI modes we could be running in
//
Expand Down Expand Up @@ -133,12 +144,12 @@ open class CustomToolbar(

if (lastInsets != newInsets) {
lastInsets = newInsets
applyExactPadding(
lastInsets.left,
lastInsets.top,
lastInsets.right,
lastInsets.bottom,
)
// applyExactPadding(
// lastInsets.left,
// lastInsets.top,
// lastInsets.right,
// lastInsets.bottom,
// )
}

return unhandledInsets
Expand All @@ -151,15 +162,25 @@ open class CustomToolbar(
r: Int,
b: Int,
) {
RNSLog.d("Toolbar", "onLayout height=${b - t} insets\n$rootWindowInsets")
super.onLayout(hasSizeChanged, l, t, r, b)

if (lastInsets != InsetsCompat.NONE) {
laidOutAfterInsetsDispatch = true
}

config.onNativeToolbarLayout(
this,
hasSizeChanged || isForceShadowStateUpdateOnLayoutRequested,
)
isForceShadowStateUpdateOnLayoutRequested = false
}

// override fun onAttachedToWindow() {
// super.onAttachedToWindow()
// dispatchApplyWindowInsets(rootWindowInsets)
// }

fun updateContentInsets() {
contentInsetStartWithNavigation = config.preferredContentInsetStartWithNavigation
setContentInsetsRelative(config.preferredContentInsetStart, config.preferredContentInsetEnd)
Expand All @@ -171,11 +192,14 @@ open class CustomToolbar(
right: Int,
bottom: Int,
) {
requestForceShadowStateUpdateOnLayout()
RNSLog.d("Toolbar", "Toolbar: applyExactPadding($left, $top, $right, $bottom)")
setPadding(left, top, right, bottom)
requestForceShadowStateUpdateOnLayout()
// requestLayoutInCurrentLoop()
}

private fun requestForceShadowStateUpdateOnLayout() {
RNSLog.d("Toolbar", "Toolbar: Requesting shadow state update on custom toolbar padding $shouldAvoidDisplayCutout")
isForceShadowStateUpdateOnLayoutRequested = shouldAvoidDisplayCutout
}
}
6 changes: 6 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.swmansion.rnscreens.events.SheetDetentChangedEvent
import com.swmansion.rnscreens.ext.asScreenStackFragment
import com.swmansion.rnscreens.ext.parentAsViewGroup
import com.swmansion.rnscreens.gamma.common.FragmentProviding
import com.swmansion.rnscreens.utils.RNSLog

@SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated.
class Screen(
Expand Down Expand Up @@ -160,6 +161,10 @@ class Screen(
wrapper.delegate = this
}

internal fun onAddedToContainer(container: ScreenStack) {
(fragment as ScreenStackFragment).setToolbar(this.headerConfig!!.toolbar)
}

override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
// do nothing, react native will keep the view hierarchy so no need to serialize/deserialize
// view's states. The side effect of restoring is that TextInput components would trigger
Expand All @@ -184,6 +189,7 @@ class Screen(
val height = b - t

dispatchShadowStateUpdate(width, height, t)
RNSLog.d("Screen", "Screen [$id]: onLayout height=$height")

// FormSheet has no header in current model.
notifyHeaderHeightChange(t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ open class ScreenContainer(
screen.fragmentWrapper = fragment
screenWrappers.add(index, fragment)
screen.container = this
onScreenAdded(screen)
onScreenChanged()
}

open fun onScreenAdded(screen: Screen) = Unit

open fun removeScreenAt(index: Int) {
screenWrappers[index].screen.container = null
screenWrappers.removeAt(index)
Expand Down
5 changes: 5 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ class ScreenStack(
stack.forEach { it.onContainerUpdate() }
}

override fun onScreenAdded(screen: Screen) {
super.onScreenAdded(screen)
screen.onAddedToContainer(this)
}

private fun drawAndRelease() {
// We make a copy of the drawingOps and use it to dispatch draws in order to be sure
// that we do not modify the original list. There are cases when `op.draw` can call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.animation.Animation
import android.widget.LinearLayout
import androidx.appcompat.widget.Toolbar
Expand Down Expand Up @@ -210,6 +211,7 @@ class ScreenStackFragment :
AppBarLayout.LayoutParams.MATCH_PARENT,
AppBarLayout.LayoutParams.WRAP_CONTENT,
)
// this.clipToPadding = false
}

coordinatorLayout.addView(appBarLayout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ class ScreenStackHeaderConfig(
val contentInsetEnd = toolbar.currentContentInsetEnd + toolbar.paddingEnd

// Note that implementation of the callee differs between architectures.
updateHeaderConfigState(
toolbar.width,
toolbar.height,
contentInsetStart,
contentInsetEnd,
)
// updateHeaderConfigState(
// toolbar.width,
// toolbar.height,
// contentInsetStart,
// contentInsetEnd,
// )
}

override fun onLayout(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ internal class ScreensCoordinatorLayout(
PointerEventsBoxNoneImpl(),
)

override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets = super.onApplyWindowInsets(insets)
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets =
super.onApplyWindowInsets(insets)

private val animationListener: Animation.AnimationListener =
object : Animation.AnimationListener {
Expand Down
102 changes: 102 additions & 0 deletions apps/src/tests/TestFerran.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as React from 'react';

import { View, Text, StyleSheet, Button } from 'react-native';
import { NavigationContainer, useNavigation } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator, NativeStackScreenProps } from '@react-navigation/native-stack';

type RootTabParamList = {
First: undefined;
Second: undefined;
Third: undefined
};

type InnerParamList = {
Inner: undefined;
Second: undefined;
};

type Props = NativeStackScreenProps<InnerParamList, 'Inner'>;
const Tab = createBottomTabNavigator<RootTabParamList>();
const Stack = createNativeStackNavigator<InnerParamList>();

const options = {
tabBarIcon: () => null,
};

function RegularScreen({ route }: Props) {
const { name } = route;
const navigation = useNavigation();
return (
<View style={styles.container}>
<Text>{`${name} Screen`}</Text>
<Button title='Push Second' onPress={() => navigation.navigate('Second')} />
</View>
);
}

function SecondScreen({ route }: Props) {
const { name } = route;
const navigation = useNavigation();
return (
<View style={styles.container}>
<Text>{`${name} Screen`}</Text>
<Button title='Go back' onPress={() => navigation.popTo('Inner')} />
</View>
);
}


function FirstStack() {
return (
<Stack.Navigator screenOptions={{ title: 'First', statusBarStyle: 'dark' }}>
<Stack.Screen name="Inner" component={RegularScreen} />
<Stack.Screen name="Second" component={SecondScreen} />
</Stack.Navigator>
);
}

function SecondStack() {
return (
<Stack.Navigator screenOptions={{ title: 'Second', statusBarStyle: 'dark' }}>
<Stack.Screen name="Inner" component={RegularScreen} />
<Stack.Screen name="Second" component={SecondScreen} />
</Stack.Navigator>
);
}

function ThirdStack() {
return (
<Stack.Navigator screenOptions={{ title: 'Third', statusBarStyle: 'dark' }}>
<Stack.Screen name="Inner" component={RegularScreen} />
<Stack.Screen name="Second" component={SecondScreen} />
</Stack.Navigator>
);
}

function RootStack() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="First" component={FirstStack} options={options} />
<Tab.Screen name="Second" component={SecondStack} options={options} />
<Tab.Screen name="Third" component={ThirdStack} options={options} />
</Tab.Navigator>
);
}

export default function App() {
return (
<NavigationContainer>
<RootStack />
</NavigationContainer>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});

1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,4 @@ export { default as TestAnimation } from './TestAnimation';
export { default as TestBottomTabs } from './TestBottomTabs';
export { default as TestScreenStack } from './TestScreenStack';
export { default as TestSplitView } from './TestSplitView';
export { default as TestFerran } from './TestFerran';
Loading
Loading