iosreact-nativeanimationreact-native-iosreact-native-reanimated

React-native-reanimated animation not working on ios, works in android


I have an animation for a bottom bar that goes under the tab text, the animation is working fine in android but not in ios. I am using react-native-reanimated. Any help would be appreciated. Thanks

const MyTabBar = React.memo((props) => {


const {state, descriptors, navigation, position, setEnableSwipe, swipeEnabled, layout, theme, auth, ui} = props;
  if (state.routes[state.index].state && state.routes[state.index].state.index !== 0) {
    if (swipeEnabled === true) {
      setEnableSwipe(false)
    }
    return null;
  }
  else {
    if (swipeEnabled === false) {
      setEnableSwipe(true);
    }

    var tabWidth = (layout.width - 50)/3
    const left = Animated.interpolate(position, {
      inputRange: [0, 1, 2],
      outputRange: [(tabWidth - 50)/2 , tabWidth + (tabWidth - 50)/2, 2*tabWidth + (tabWidth - 50)/2]
    });
    const length = Animated.interpolate(position, {
      inputRange: [0, 0.5, 1, 1.5, 2],
      outputRange: [0.3, 1, 0.3, 1, 0.3],
    })
    return (
      <View style={{ flexDirection: 'row', backgroundColor: Platform.OS === 'ios' && ui.showing_modal ? 'white' : 'white', alignItems: 'center' }}>
        {state.routes.map((route, index) => {
          const { options } = descriptors[route.key];
          const label =
            options.tabBarLabel !== undefined
              ? options.tabBarLabel
              : options.title !== undefined
              ? options.title
              : route.name;
  
          const isFocused = state.index === index;
  
          const onPress = () => {
            const event = navigation.emit({
              type: 'tabPress',
              target: route.key,
              canPreventDefault: true,
            });
  
            if (!isFocused && !event.defaultPrevented) {
              navigation.navigate(route.name);
            }
          };
  
          const onLongPress = () => {
            navigation.emit({
              type: 'tabLongPress',
              target: route.key,
            });
          };

          const inputRange = state.routes.map((_, i) => i);
          const opacity = Animated.interpolate(position, {
            inputRange,
            outputRange: inputRange.map(i => (i === index ? 1 : 0.4)),
          });

          return (
            <TouchableOpacity
              accessibilityRole="button"
              accessibilityStates={isFocused ? ['selected'] : []}
              accessibilityLabel={options.tabBarAccessibilityLabel}
              testID={options.tabBarTestID}
              onPress={onPress}
              onLongPress={onLongPress}
              style={{flex: 1}}
            >
              <Animated.Text style={{ 
                opacity, 
                fontWeight: index === state.index ? '600' : 'normal', 
                fontSize: index === state.index ? 19 : 17,
                textAlign: 'center',
              }}>
                {label}
              </Animated.Text>
            </TouchableOpacity>
          );
        })}
         {Platform.OS === 'ios' && false ? 
      <View/> :  <Animated.View 
      style={{
        backgroundColor: theme.primaryColor, 
        translateX: left, 
        scaleX: length,
        height: 4, 
        width: 50, 
        position: 'absolute', 
        bottom: 0,
        borderRadius: 10,
      }} />}
        <TouchableOpacity 
          style={{minWidth: 50, maxWidth: 50}}
          onPress={() => {
            switch (state.index) {
              case 0:
                navigation.navigate('AddSchedule');
                break;
              case 1:
                navigation.navigate('AddScene');
                break;
              case 2:
                if (auth.accesstoken) {
                  navigation.navigate('NewGeoscene');
                } else {
                  ReactNativeHapticFeedback.trigger('notificationWarning', {
                    ignoreAndroidSystemSettings: true,
                    enableVibrateFallback: true
                  }) 
                }
                break;
              default:
                //
            }
          }}
        > 
          <Text style={{fontSize: 36, color: theme.primaryColor}}> + </Text>
        </TouchableOpacity>
      </View>
    );
  }
})

this is my code, the line Animated.View doesn't animate in iOS so I am not rendering it there, but i want it working.

Android Expected behaviour (android)

iOS: behaviour in iOS


Solution

  • I have an observation from my own code which may or may not be applicable here. This observation is only true if both of the transform properties are using react-native-reanimated animation values. Your code appears to match this scenario.

    In iOS only, I must place the scale property before the translateX property in my transform object. If I put the translateX property before the scale property then the translateX property is impeded.

    I have no explanation for this, but I have tested it with scaleX since that's what you are using and the same is true.

    So for clarity, the following works:

    <Animated.Value
      style={[{
        transform: [{
          scale: animationValue1,
          translateX: animationValue2,
        }]
      }]}
    />
    

    ..while in the next, the scale works but the translateX does not:

    <Animated.Value
      style={[{
        transform: [{
          translateX: animationValue2,
          scale: animationValue1,
        }]
      }]}
    />
    

    You'll notice my syntax is a little different from yours too. As far as I'm aware you shouldn't be putting the translateX and scaleX properties in the style object without being wrapped in a transform property, as above. Perhaps reanimated's View works a bit differently to react native's native one.. If it's still not working after you try reordering the properties then give this some thought too.

    See the documentation here: https://reactnative.dev/docs/transforms#transform

    It's a bit confused because the API describes transform as a function, but the example at the top of the page shows it used as I have done above.