Search code examples
typescriptreact-nativeexpodropdownreact-native-flatlist

Dropdown but per index choice in React Native


so i want to make that each of the flatlist dropdown open per index, so when you choose 1 section it will only open that section so the other doesn't get open, but sadly i still can not find the solution

The Code

App.tsx:

import React, { useRef, useState } from 'react';
import { View, Pressable, StyleSheet, Text, FlatList } from 'react-native';
import { AnimateHeight } from './animate-height';
import { Ionicons } from '@expo/vector-icons';
import { MotiView } from 'moti';
import Constants from 'expo-constants';

export default function App() {
  const ref = useRef()
  const dummyData = [
    {
      id: 1,
      question: "are u a banana?",
      desc: "no i'm not",
    },
     {
      id: 2,
      question: "are u a apple?",
      desc: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
    },
  ]
  const [currentIndex, setCurrentIndex] = useState(0);
  const [show, toggle] = React.useReducer((open) => !open, false);

  const renderItem = ({item, index}:any) => {
    return(
    <View style={styles.screen}>
      <View style={itemStyles.container}>
      <Pressable onPress={toggle} style={itemStyles.question}>
        <Text selectable={false} style={itemStyles.questionText}>
          {item?.question}
        </Text>
        <MotiView
          animate={{
            rotateZ: show ? '-89deg' : '90deg',
          }}>
          <Ionicons name="chevron-forward" color="white" size={17} />
        </MotiView>
      </Pressable>
      <AnimateHeight enterFrom="bottom" hide={!show}>
          <View style={itemStyles.answer}>
            <Text style={itemStyles.answerText}>
              {item?.desc}
            </Text>
          </View>
        </AnimateHeight>
    </View>
    </View>
    )
  }

  return (
    <View>
      <FlatList 
        data={dummyData}
        renderItem={renderItem}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    backgroundColor: '#161618',
    paddingTop: Constants.statusBarHeight,
  },
});

const itemStyles = StyleSheet.create({
  question: {
    padding: 16,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  answer: {
    padding: 16,
    marginTop: -16
  },
  answerText: {
    color: '#A09FA5',
    lineHeight: 20
  },
  container: {
    borderBottomWidth: 1,
    borderBottomColor: '#232326',
  },
  questionText: {
    color: '#EDEDEE',
    fontWeight: 'bold',
  },
});

animate-height.tsx:

import '@motify/core';
import '@motify/components';

import React from 'react';
import { MotiView, TransitionConfig, useDynamicAnimation } from 'moti';
import { StyleSheet, Platform } from 'react-native';
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';
import { View } from 'react-native';

type Props = {
  children?: React.ReactNode;
  /**
   * If `true`, the height will automatically animate to 0. Default: `false`.
   */
  hide?: boolean; 
  onHeightDidAnimate?: (height: number) => void;
  /**
   * Defines where the expanded view will be anchored.
   *
   * Default: `top`
   *
   * This prop is untested, use with caution
   */
  enterFrom?: 'bottom' | 'top';
  initialHeight?: number;
} & React.ComponentProps<typeof MotiView>;

function AnimateHeight({
  children,
  hide = false,
  style,
  delay = Platform.select({ web: 250, default: 0 }),
  transition = { type: 'timing', delay }, 
  enterFrom = 'top',
  onHeightDidAnimate,
  initialHeight = 0,
  ...motiViewProps
}: Props) {
  const measuredHeight = useSharedValue(initialHeight);
  const state = useDynamicAnimation(() => {
    return {
      height: initialHeight,
      opacity: !initialHeight || hide ? 0 : 1
    }
  })
  if ('state' in motiViewProps) {
    console.warn('[AnimateHeight] state prop not supported')
  }

  const animation = useDerivedValue(() => {
    let height = Math.ceil(measuredHeight.value);
    if (hide) {
      height = 0;
    } 

    const notVisible = !height || hide;
    
    state.animateTo({
      height,
      opacity: !height || hide ? 0 : 1
    });
  }, [hide, measuredHeight]);


  return (
    <MotiView
      {...motiViewProps}
      state={state}
      transition={transition}
      onDidAnimate={
        onHeightDidAnimate &&
        ((key, finished, _, { attemptedValue }) =>
          key == 'height' && onHeightDidAnimate(attemptedValue as number))
      }
      style={[styles.hidden, style]}>
      <View
        style={[
          StyleSheet.absoluteFill,
          styles.autoBottom

          // THIS BREAKS IDK WHY, so ignore that prop
          // enterFrom === 'top' ? styles.autoBottom : styles.autoTop, 
        ]}
        onLayout={({ nativeEvent }) => {
          measuredHeight.value = nativeEvent.layout.height;
        }}>
        {children}
      </View>
    </MotiView>
  );
}

const styles = StyleSheet.create({
  autoBottom: {
    bottom: 'auto',
  },
  autoTop: {
    top: 'auto',
  },
  hidden: {
    overflow: 'hidden',
  },
});

export { AnimateHeight };

and this is the Expo Snacks if you want to try it live: https://snack.expo.dev/@mikess/moti-animated-heigt

if anyone can help or find the solution it will be a help Thank you very much


Solution

  • try this,

    App.tsx

    import React, { useState } from 'react';
    import { View, Pressable, StyleSheet, Text, FlatList } from 'react-native';
    import { AnimateHeight } from './animated-height';
    import { Ionicons } from '@expo/vector-icons';
    import { MotiView } from 'moti';
    import Constants from 'expo-constants';
    
    export default function App() {
      const dummyData = [
        {
          id: 1,
          question: "are u a banana?",
          desc: "no i'm not",
        },
         {
          id: 2,
          question: "are u a apple?",
          desc: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
        },
      ]
      const [currentIndex, setCurrentIndex] = useState(0);
      const [show, toggle] = React.useReducer((open) => !open, false);
    
      const renderItem = ({item, index}:any) => {
        return(
        <View style={styles.screen}>
          <View style={itemStyles.container}>
          <Pressable onPress={()=> setCurrentIndex(index)} style={itemStyles.question}>
            <Text selectable={false} style={itemStyles.questionText}>
              {item?.question}
            </Text>
            <MotiView
              animate={{
                rotateZ: currentIndex == index ? '-90deg' : '0deg',}}>
              <Ionicons name="chevron-forward" color="white" size={17} />
            </MotiView>
          </Pressable>
          <AnimateHeight enterFrom="bottom" hide={currentIndex == index ? show : !show}>
              <View style={itemStyles.answer}>
                <Text style={itemStyles.answerText}>
                  {item?.desc}
                </Text>
              </View>
            </AnimateHeight>
        </View>
        </View>
        )
      }
    
      return (
        <View>
          <FlatList 
            data={dummyData}
            renderItem={renderItem}
            keyExtractor={(index)=> index.toString()}
          />
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      screen: {
        flex: 1,
        backgroundColor: '#161618',
        paddingTop: Constants.statusBarHeight,
      },
    });
    
    const itemStyles = StyleSheet.create({
      question: {
        padding: 16,
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
      },
      answer: {
        padding: 16,
        marginTop: -16
      },
      answerText: {
        color: '#A09FA5',
        lineHeight: 20
      },
      container: {
        borderBottomWidth: 1,
        borderBottomColor: '#232326',
      },
      questionText: {
        color: '#EDEDEE',
        fontWeight: 'bold',
      },
    });