react-navigation-shared-element
react-navigation-shared-element copied to clipboard
[iOS] Offset when using SafeAreaView
First of all, thank you for this amazing library!
I found an issue in iOS when using SafeAreaView (both from react-native and react-native-safe-area-context) with SharedElements. Without the SafeAreaView it works well. When making the animation, there is a little "jump", where the final place of the animation is off by some offset.
The code can be found and it's runnable in https://github.com/BeeMargarida/react-navigation-shared-element, it's the last example (sorry for the quality of the code, it was a testing scenario made really fast).
Code
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import React from "react";
import { Button, View } from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import {
SharedElement,
createSharedElementStackNavigator,
} from "react-navigation-shared-element";
const Stack = createStackNavigator();
const SharedElementStack = createSharedElementStackNavigator();
const forFade = ({ current, closing }) => ({
cardStyle: {
opacity: current.progress,
},
});
const Screen1 = (props: any) => {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ height: 150, width: 150 }}>
<SharedElement style={{ flex: 1 }} id="item">
<View style={{ height: 150, width: 150, backgroundColor: "red" }}>
<View
style={{
height: 150,
width: 150,
backgroundColor: "yellow",
borderRadius: 500,
}}
/>
</View>
</SharedElement>
</View>
<Button
title="press"
onPress={() => {
props.navigation.navigate("screen2");
}}
/>
</SafeAreaView>
);
};
const Screen2 = () => {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, flexDirection: "column" }}>
<View style={{ flex: 1 }} />
<View style={{ flex: 1 }}>
<SharedElement style={{ flex: 1, alignItems: "center" }} id="item">
<View style={{ height: 300, width: 300, backgroundColor: "blue" }}>
<View
style={{
height: 300,
width: 300,
backgroundColor: "yellow",
borderRadius: 500,
}}
/>
</View>
</SharedElement>
</View>
</View>
</SafeAreaView>
);
};
const InnerStack = () => (
<SharedElementStack.Navigator mode="modal" headerMode="none">
<SharedElementStack.Screen
name="screen1"
component={Screen1}
options={{
cardStyleInterpolator: forFade,
}}
/>
<SharedElementStack.Screen
name="screen2"
component={Screen2}
sharedElements={(route, otherRoute, showing) => {
const { model } = route.params;
return [
{
id: "item",
otherId: "item",
animation: "move",
},
];
}}
/>
</SharedElementStack.Navigator>
);
export default () => (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator
initialRouteName="root"
screenOptions={{ headerShown: false }}
>
<Stack.Screen name="root" component={InnerStack} />
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
https://user-images.githubusercontent.com/25725586/130613627-91929813-9b74-491c-a59a-3220db436186.mp4
Hi, and thanks for sharing the elaborate example, that was very useful. I've added a test-case to the example app and was able to reproduce similar problems. Both iOS and Android seem to be experiencing problems. I've had a closer look at the native iOS SafeAreaView implementation in react-native-safe-area-context and I sort of see what's going on. The problem is in the fact that <SafeAreaView> needs an extra pass to the React Native UIManager and Yoga to set the padding on the view. Both the iOS and Android implementations seem to do this in a similar way. This causes react-native-shared-element to measure the size and position of the element before <SafeAreaView> has applied its padding, causing the shift that you see.
I'm not really sure on how to fix this tbh. react-native-shared-element measures your view as fast as possible (to prevent and flickering/ghost elements). For the best results you should construct your views in such a way that they don't require and re-layouting. And unfortunately <SafeAreaView> causes such re-layouting, even though it is performed at the native level.
Hi, and thanks for sharing the elaborate example, that was very useful. I've added a test-case to the
exampleapp and was able to reproduce similar problems. Both iOS and Android seem to be experiencing problems. I've had a closer look at the native iOSSafeAreaViewimplementation inreact-native-safe-area-contextand I sort of see what's going on. The problem is in the fact that<SafeAreaView>needs an extra pass to the React Native UIManager and Yoga to set thepaddingon the view. Both the iOS and Android implementations seem to do this in a similar way. This causesreact-native-shared-elementto measure the size and position of the element before<SafeAreaView>has applied its padding, causing the shift that you see.I'm not really sure on how to fix this tbh.
react-native-shared-elementmeasures your view as fast as possible (to prevent and flickering/ghost elements). For the best results you should construct your views in such a way that they don't require and re-layouting. And unfortunately<SafeAreaView>causes such re-layouting, even though it is performed at the native level.
Thank you for you very fast response!
Just to get your opinion, would implement the safe area logic locally (use the insets initialWindowMetrics from "react-native-safe-area-context" as padding for the view of each screen) solve this problem?
Not sure, but I would definitely try 👍
Alternatively, you could try caching the insets so that they are immediately available when your view is rendered for the first time.
Alternatively, you could try caching the insets so that they are immediately available when your view is rendered for the first time.
It seems caching does not seem to work as well. Either way, thank you!
https://github.com/th3rdwave/react-native-safe-area-context#usesafeareainsets works for me
import {useSafeAreaInsets} from "react-native-safe-area-context";
const insets = useSafeAreaInsets();
const Screen = () => {
return (
<View style={{
flex: 1,
paddingTop: insets.top,
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: insets.bottom
}}>
<SharedElement id="itemId">
</SharedElement>
</View>
);
};
The navigation option headerShown:false makes the offset and jump bigger when using <SafeAreaView>
Creating a view as suggested above with padding solves the issue for me :)
Having this issue as well, would be great to see a solution. Specifically with the KeyboardAvoidingView