Search code examples
reactjsreact-nativereact-animated

Make animated collapsible card component, with initial props to show or hide


Background

Using React Native I was able to make collapsible card component. On Icon click the card slides up hiding its content, or expands showing its content. I would think setting the default value would be as easy as setting expanded to false or true, but I think the problem here is that when it is toggled an animation is triggered which changes the height of the card.

Example

class CardCollapsible extends Component{
  constructor(props){
    super(props);

    this.state = {
      title: props.title,
      expanded: true,
      animation: new Animated.Value(),
      iconExpand: "keyboard-arrow-down",
    };
  }

  _setMaxHeight(event){
      this.setState({
          maxHeight   : event.nativeEvent.layout.height
      });
  }

  _setMinHeight(event){
      this.setState({
          minHeight   : event.nativeEvent.layout.height
      });

      this.toggle = this.toggle.bind(this);
  }

  toggle(){
    let initialValue    = this.state.expanded? this.state.maxHeight + this.state.minHeight : this.state.minHeight,
        finalValue      = this.state.expanded? this.state.minHeight : this.state.maxHeight + this.state.minHeight;

    this.setState({
      expanded : !this.state.expanded
    });

    if (this.state.iconExpand === "keyboard-arrow-up") {
      this.setState({
        iconExpand : "keyboard-arrow-down"
      })
    } else {
      this.setState({
        iconExpand : "keyboard-arrow-up"
      })
    }
    this.state.animation.setValue(initialValue);
    Animated.spring( this.state.animation, {
        toValue: finalValue
      }
    ).start();
  }

  render(){

    return (
      <Animated.View style={[styles.container,{height: this.state.animation}]}>
          <View style={styles.titleContainer} onLayout={this._setMinHeight.bind(this)}>
            <CardTitle>{this.state.title}</CardTitle>
            <TouchableHighlight
              style={styles.button}
              onPress={this.toggle}
              underlayColor="#f1f1f1">
              <Icon
                name={this.state.iconExpand}
                style={{ fontSize: 30 }}/>
            </TouchableHighlight>
          </View>
          <Separator />
          <View style={styles.card} onLayout={this._setMaxHeight.bind(this)}>
            {this.props.children}
          </View>
      </Animated.View>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
    margin:10,
    overflow:'hidden'
    },
  titleContainer: {
    flexDirection: 'row'
    },
  card: {
    padding: 10
  }
});

export { CardCollapsible };

Open

enter image description here

Closed

enter image description here

Question

My goal is to allow a person calling the component to set the initial state of the component to expanded or open. But when I try changing the expanded state to false it does not render closed.

How would I go about allowing the user calling the component to select whether it is expanded or closed on initial component render?


Solution

  • Made a brand new one for you. Simple and works fine.

    Note: no state required for this component. fewer state, better performance.

    Maybe you could modify your own style on top of this =)

    class Card extends Component {
        anime = {
            height: new Animated.Value(),
            expanded: false,
            contentHeight: 0,
        }
    
        constructor(props) {
            super(props);
    
            this._initContentHeight = this._initContentHeight.bind(this);
            this.toggle = this.toggle.bind(this);
    
            this.anime.expanded = props.expanded;
        }
    
        _initContentHeight(evt) {
            if (this.anime.contentHeight>0) return;
            this.anime.contentHeight = evt.nativeEvent.layout.height;
            this.anime.height.setValue(this.anime.expanded ? this._getMaxValue() : this._getMinValue() );
        }
    
        _getMaxValue() { return this.anime.contentHeight };
        _getMinValue() { return 0 };
    
        toggle() {
            Animated.timing(this.anime.height, {
                toValue: this.anime.expanded ? this._getMinValue() : this._getMaxValue(),
                duration: 300,
            }).start();
            this.anime.expanded = !this.anime.expanded;
        }
    
        render() {
            return (
                <View style={styles.titleContainer}>
                    <View style={styles.title}>
                        <TouchableHighlight underlayColor="transparent" onPress={this.toggle}>
                            <Text>{this.props.title}</Text>
                        </TouchableHighlight>
                    </View>
    
                    <Animated.View style={[styles.content, { height: this.anime.height }]} onLayout={this._initContentHeight}>
                        {this.props.children}
                    </Animated.View>
                </View>
            );
        }
    }
    

    Usage:

    <Card title='Customized Card 1' expanded={false}>
        <Text>Hello, this is first line.</Text>
        <Text>Hello, this is second line.</Text>
        <Text>Hello, this is third line.</Text>
    </Card>
    

    Visual result: (only second card start with expanded={true}, others with expanded={false})

    enter image description here