software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
9.09k stars 1.32k forks source link

useAnimatedStyle causing focus and button press issues in FlatList #6704

Open ravis-farooq opened 1 week ago

ravis-farooq commented 1 week ago

Description

I am encountering a problem when using animations with FlatList. Specifically, when I use Animated or useAnimatedStyle from Reanimated for tab animations, the following issues occur:

The TextInput in the second tab (and onwards) doesn't gain focus when tapped. Any TouchableOpacity or button inside the FlatList items also becomes unresponsive. When I switched from Reanimated's useAnimatedStyle to Animated from React Native, the issue was resolved, and everything worked as expected. This appears to be a problem with how useAnimatedStyle interacts with FlatList.

Steps to reproduce

Copy the code below into a fresh React Native project. Run the app. Try focusing on the TextInput in the second tab or pressing any button inside the second tab. Comment out Reanimated usage and use React Native's Animated instead to observe the difference.

import React, { useState, useRef } from 'react';
import {
  View,
  TextInput,
  FlatList,
  StyleSheet,
  Dimensions,
  Animated,
  TouchableOpacity,
} from 'react-native';
import { Box, Typography } from '@shopify/restyle';

const { width } = Dimensions.get('window');
const spacing = 16;

type TabData = {
  id: number;
  title: string;
  placeholder: string;
};

const CustomTabView = () => {
  const [tabsData] = useState<TabData[]>([
    { id: 1, title: 'Tab 1', placeholder: 'Enter text for Tab 1' },
    { id: 2, title: 'Tab 2', placeholder: 'Enter text for Tab 2' },
    { id: 3, title: 'Tab 3', placeholder: 'Enter text for Tab 3' },
  ]);

  const [tabValues, setTabValues] = useState<{ [key: number]: string }>({
    1: '',
    2: '',
    3: '',
  });

  const [activeTab, setActiveTab] = useState(0);
  const flatListRef = useRef<FlatList>(null);
  const animatedIndicator = useRef(new Animated.Value(0)).current;

  const handleTabPress = (index: number) => {
    setActiveTab(index);

    // Animate the indicator to the selected tab
    Animated.timing(animatedIndicator, {
      toValue: index * (width / tabsData.length),
      duration: 250,
      useNativeDriver: false,
    }).start();

    flatListRef.current?.scrollToIndex({
      animated: true,
      index,
    });
  };

  const handleInputChange = (id: number, value: string) => {
    setTabValues((prevState) => ({ ...prevState, [id]: value }));
  };

  return (
    <View style={styles.container}>
      {/* Tab Buttons with Animated Indicator */}
      <Box
        flexDirection="row"
        justifyContent="space-around"
        bg="gray200"
        position="absolute"
        width={width - spacing}
        top={0}
        zIndex={1}
        borderRadius="s"
        overflow="hidden"
        style={{
          padding: spacing / 3,
          marginHorizontal: spacing / 2,
        }}
      >
        <Animated.View
          style={[
            styles.indicator,
            {
              width: width / tabsData.length,
              transform: [{ translateX: animatedIndicator }],
            },
          ]}
        />
        {tabsData.map((tab, index) => (
          <TouchableOpacity
            key={index}
            style={styles.tabButton}
            onPress={() => handleTabPress(index)}
          >
            <Typography
              textAlign="center"
              fontFamily="medium"
              color={activeTab === index ? 'globalWhite' : 'textSecondary'}
            >
              {tab.title}
            </Typography>
          </TouchableOpacity>
        ))}
      </Box>

      {/* Content for Each Tab */}
      <FlatList
        ref={flatListRef}
        data={tabsData}
        renderItem={({ item }) => (
          <View style={styles.tabContainer} key={item.id}>
            <TextInput
              style={styles.textInput}
              placeholder={item.placeholder}
              value={tabValues[item.id]}
              onChangeText={(text) => handleInputChange(item.id, text)}
              onFocus={() => console.log(`Focused on Tab ${item.id}`)}
            />
          </View>
        )}
        keyExtractor={(item) => item.id.toString()}
        horizontal
        showsHorizontalScrollIndicator={false}
        pagingEnabled
        keyboardShouldPersistTaps="always"
        removeClippedSubviews={false} // Important to ensure inputs offscreen remain interactive
        onMomentumScrollEnd={(event) => {
          const index = Math.floor(event.nativeEvent.contentOffset.x / width);
          setActiveTab(index);
          Animated.timing(animatedIndicator, {
            toValue: index * (width / tabsData.length),
            duration: 250,
            useNativeDriver: false,
          }).start();
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 100, // Space for tab bar at the top
  },
  tabContainer: {
    width,
    justifyContent: 'center',
    alignItems: 'center',
  },
  textInput: {
    width: '80%',
    borderColor: '#ccc',
    borderWidth: 1,
    padding: 10,
    borderRadius: 5,
  },
  tabButton: {
    flex: 1,
    alignItems: 'center',
  },
  indicator: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    backgroundColor: 'blue',
    borderRadius: 10,
  },
});

export default CustomTabView;

Snack or a link to a repository

https://snack.expo.dev/_cgYFhPQvKDe1htoimwii

Reanimated version

3.16.1

React Native version

0.76

Platforms

Android

JavaScript runtime

None

Workflow

None

Architecture

None

Build type

None

Device

None

Device model

No response

Acknowledgements

Yes