3

I want to implement an animated accordion list/ drawer / drop-down menu / collapsible card.

The animation should be performant and look like this:

enter image description here

Vipul
  • 734
  • 1
  • 10
  • 27

3 Answers3

14

After a lot of searching, I could find many libraries. But I wanted to implement it without any library. Also, some tutorials showed how to build one, but they were not performant.

Finally, this is how I implemented it. The complete snack code is here: https://snack.expo.dev/@vipulchandra04/a85348

I am storing isOpen (whether the menu is open or closed) in a state. Then changing that state on button press. I am using the LayoutAnimation API in React-Native to animate the opening and closing of the list. LayoutAnimation runs the animation natively, thus it is performant.

const Accordion = ({ title, children }) => {
  const [isOpen, setIsOpen] = useState(false);

  const toggleOpen = () => {
    setIsOpen(value => !value);
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
  }

  return (
    <>
      <TouchableOpacity onPress={toggleOpen} activeOpacity={0.6}>
        {title}
      </TouchableOpacity>
      <View style={[styles.list, !isOpen ? styles.hidden : undefined]}>
        {children}
      </View>
    </>
  );
};

const styles = StyleSheet.create({
  hidden: {
    height: 0,
  },
  list: {
    overflow: 'hidden'
  },
});

enter image description here

Vipul
  • 734
  • 1
  • 10
  • 27
  • hmmmm. tks you for demo. Are you still following this post? it has a problem. If we spam accordion with click, children's opacity descending to 0. – Zuet Apr 16 '22 at 03:31
  • @Zuet Can you share an expo link reproducing your problem? Also, have you tried with a physical device instead of the simulator? Sometimes animations lag on the simulator. – Vipul Apr 16 '22 at 17:48
  • I just test your expo on expo mobile. if click accordion twice so fast, children's opacity descending to 0. – Zuet Apr 18 '22 at 01:12
  • On android `LayoutAnimation` is experimental, which may be causing the problem. – Vipul Apr 18 '22 at 08:02
1

With this, it will fix the Vipul's demo's error: if click accordion so fast, children's opacity descending to 0. and add animation for icon

import {
  Animated,
  LayoutAnimation,
  Platform,
  StyleProp,
  StyleSheet,
  UIManager,
  View,
  ViewStyle,
} from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons;

if (
  Platform.OS === 'android' &&
  UIManager.setLayoutAnimationEnabledExperimental
) {
  UIManager.setLayoutAnimationEnabledExperimental(true);
}

const toggleAnimation = duration => {
  return {
    duration: duration,
    update: {
      property: LayoutAnimation.Properties.scaleXY,
      type: LayoutAnimation.Types.easeInEaseOut,
    },
    delete: {
      property: LayoutAnimation.Properties.opacity,
      type: LayoutAnimation.Types.easeInEaseOut,
    },
  };
};

interface IAccordion {
  title?: JSX.Element | JSX.Element[];
  children?: JSX.Element | JSX.Element[];
  style?: StyleProp<ViewStyle> | undefined;
}

const Accordion = ({title, children, style}: IAccordion) => {
  const [isOpen, setIsOpen] = useState(false);
  const animationController = useRef(new Animated.Value(0)).current;

  const arrowTransform = animationController.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '90deg'],
  });

  const onToggle = () => {
    setIsOpen(prevState => !prevState);

    const duration = 300;
    const config = {
      duration: duration,
      toValue: isOpen ? 0 : 1,
      useNativeDriver: true,
    };
    Animated.timing(animationController, config).start();
    LayoutAnimation.configureNext(toggleAnimation(duration));
  };

  return (
    <View style={style ? style : styles.accordion}>
      <TouchableOpacity onPress={onToggle} style={styles.heading}>
        {title}
        <Animated.View style={{transform: [{rotateZ: arrowTransform}]}}>
          <Ionicons name={'chevron-forward-outline'} size={18} />
        </Animated.View>
      </TouchableOpacity>
      <View style={[styles.list, !isOpen ? styles.hidden : undefined]}>
        {children}
      </View>
    </View>
  );
};

export default Accordion;
Zuet
  • 559
  • 6
  • 23
0

I had difficulty using the native API, so I go to third parties. The only thing I couldn't do was make the accordion size automatic.

import { useEffect } from 'react';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import styled from 'styled-components';

const Accordion = ({ children, open, height }) => {
  const heightAnimation = useSharedValue(0);

  useEffect(() => {
    if (open === true) heightAnimation.value = height;
    if (open === false) heightAnimation.value = 0;
  }, [open]);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      height: withTiming(heightAnimation.value, {
        duration: 500,
        easing: Easing.bezier(0.25, 0.1, 0.25, 1),
      }),
    };
  });

  return (
    <>
      <Main style={animatedStyle}>{children}</Main>
    </>
  );
};

const Main = styled(Animated.View)`
  width: 100%;
  align-items: center;
  justify-content: center;
  overflow: hidden;
`;

export default Accordion;

Using:

<Accordion height={height} open={open}>
{children}
</Accordion>

As asked here for an example of what I managed to do with it, I tried to get as much out of it as possible.

You can see a deploy here: https://snack.expo.dev/@francisco.ossian/accordion

Libs used, react-native-reanimated

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Jatin Bhuva Dec 29 '22 at 11:33