react-navigation-shared-element icon indicating copy to clipboard operation
react-navigation-shared-element copied to clipboard

[iOS] Offset when using SafeAreaView

Open BeeMargarida opened this issue 4 years ago • 8 comments

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

BeeMargarida avatar Aug 24 '21 12:08 BeeMargarida

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.

IjzerenHein avatar Aug 24 '21 15:08 IjzerenHein

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.

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?

BeeMargarida avatar Aug 24 '21 15:08 BeeMargarida

Not sure, but I would definitely try 👍

IjzerenHein avatar Aug 24 '21 15:08 IjzerenHein

Alternatively, you could try caching the insets so that they are immediately available when your view is rendered for the first time.

IjzerenHein avatar Aug 24 '21 15:08 IjzerenHein

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!

BeeMargarida avatar Aug 24 '21 15:08 BeeMargarida

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>
    );
};

aggr2150 avatar Sep 10 '21 14:09 aggr2150

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 :)

edvard-bjarnason avatar Nov 15 '22 15:11 edvard-bjarnason

Having this issue as well, would be great to see a solution. Specifically with the KeyboardAvoidingView

GhostWalker562 avatar Nov 05 '23 19:11 GhostWalker562