Skip to content

Instantly share code, notes, and snippets.

@ajiehatajie
Created September 30, 2017 18:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ajiehatajie/c028d94065814337ad6d0311392084c8 to your computer and use it in GitHub Desktop.
Save ajiehatajie/c028d94065814337ad6d0311392084c8 to your computer and use it in GitHub Desktop.
/** * Created by f.putra on 6/22/17. */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import {ViewPropTypes} from 'react-native'; import { Animated, Easing, View, } from 'react-native'; const ANIMATED_EASING_PREFIXES = ['easeInOut', 'easeOut', 'easeIn']; class Collapsible extends Component { static propTypes = { align: PropTypes.oneOf(['top', 'center', 'bottom']), collapsed: PropTypes.bool, collapsedHeight: PropTypes.number, duration: PropTypes.number, easing: PropTypes.oneOfType([ PropTypes.string, PropTypes.func, ]), style: ViewPropTypes.style, }; static defaultProps = { align: 'top', collapsed: true, collapsedHeight: 0, duration: 300, easing: 'easeOutCubic', }; componentWillReceiveProps(nextProps) { if (nextProps.collapsed !== this.props.collapsed) { this._toggleCollapsed(nextProps.collapsed); } else if (nextProps.collapsed && nextProps.collapsedHeight !== this.props.collapsedHeight) { this.state.height.setValue(nextProps.collapsedHeight); } } constructor(props) { super(props); this.state = { measuring: false, measured: false, height: new Animated.Value(props.collapsedHeight), contentHeight: 0, animating: false, }; } contentHandle = null; _handleRef = (ref) => { this.contentHandle = ref; } _measureContent(callback) { this.setState({ measuring: true, }, () => { requestAnimationFrame(() => { if (!this.contentHandle) { this.setState({ measuring: false, }, () => callback(this.props.collapsedHeight)); } else { this.contentHandle.getNode().measure((x, y, width, height) => { this.setState({ measuring: false, measured: true, contentHeight: height, }, () => callback(height)); }); } }); }); } _toggleCollapsed(collapsed) { if (collapsed) { this._transitionToHeight(this.props.collapsedHeight) } else if (!this.contentHandle) { if (this.state.measured) { this._transitionToHeight(this.state.contentHeight) } return; } else { this._measureContent(contentHeight => { this._transitionToHeight(contentHeight); }) } } _transitionToHeight(height) { const { duration } = this.props; let easing = this.props.easing; if (typeof easing === 'string') { let prefix; let found = false; for (let i = 0; i < ANIMATED_EASING_PREFIXES.length; i++) { prefix = ANIMATED_EASING_PREFIXES[i]; if (easing.substr(0, prefix.length) === prefix) { easing = easing.substr(prefix.length, 1).toLowerCase() + easing.substr(prefix.length + 1); prefix = prefix.substr(4, 1).toLowerCase() + prefix.substr(5); easing = Easing[prefix](Easing[easing || 'ease']); found = true; break; } } if (!found) { easing = Easing[easing]; } if (!easing) { throw new Error('Invalid easing type "' + this.props.easing + '"'); } } if (this._animation) { this._animation.stop(); } this.setState({ animating: true }); this._animation = Animated.timing(this.state.height, { toValue: height, duration, easing, }).start(event => this.setState({ animating: false })); } _handleLayoutChange = (event) => { const contentHeight = event.nativeEvent.layout.height; if (this.state.animating || this.props.collapsed || this.state.measuring || this.state.contentHeight === contentHeight) { return; } this.state.height.setValue(contentHeight); this.setState({ contentHeight }); }; render() { const { collapsed } = this.props; const { height, contentHeight, measuring, measured } = this.state; const hasKnownHeight = !measuring && (measured || collapsed); const style = hasKnownHeight && { overflow: 'hidden', height: height, }; const contentStyle = {}; if (measuring) { contentStyle.position = 'absolute', contentStyle.opacity = 0; } else if (this.props.align === 'center') { contentStyle.transform = [{ translateY: height.interpolate({ inputRange: [0, contentHeight], outputRange: [contentHeight / -2, 0], }), }]; } else if (this.props.align === 'bottom') { contentStyle.transform = [{ translateY: height.interpolate({ inputRange: [0, contentHeight], outputRange: [-contentHeight, 0], }), }]; } return ( <Animated.View style={style} pointerEvents={collapsed ? 'none' : 'auto'}> <Animated.View ref={this._handleRef} style={[this.props.style, contentStyle]} onLayout={this.state.animating ? undefined : this._handleLayoutChange}> {this.props.children} </Animated.View> </Animated.View> ); } } export default Collapsible; /** * Created by f.putra on 6/22/17. */ import React, { Component, } from 'react'; import PropTypes from 'prop-types'; import {ViewPropTypes} from 'react-native' import { View, TouchableOpacity, } from 'react-native'; import Collapsible from './Collapsible'; const COLLAPSIBLE_PROPS = Object.keys(Collapsible.propTypes); const VIEW_PROPS = Object.keys(ViewPropTypes); class Accordion extends Component { static propTypes = { sections: PropTypes.array.isRequired, renderHeader: PropTypes.func.isRequired, renderContent: PropTypes.func.isRequired, onChange: PropTypes.func, align: PropTypes.oneOf(['top', 'center', 'bottom']), duration: PropTypes.number, easing: PropTypes.string, initiallyActiveSection: PropTypes.number, activeSection: PropTypes.oneOfType([ PropTypes.bool, // if false, closes all sections PropTypes.number, // sets index of section to open ]), underlayColor: PropTypes.string, }; static defaultProps = { underlayColor: 'black', }; constructor(props) { super(props); // if activeSection not specified, default to initiallyActiveSection this.state = { activeSection: props.activeSection !== undefined ? props.activeSection : props.initiallyActiveSection, }; } _toggleSection(section) { const activeSection = this.state.activeSection === section ? false : section; console.log(this.state.activeSection == section) if (this.props.activeSection === undefined) { this.setState({activeSection}); } if (this.props.onChange) { this.props.onChange(activeSection); } } componentWillReceiveProps(nextProps) { if (nextProps.activeSection !== undefined) { this.setState({ activeSection: nextProps.activeSection, }); } } render() { let viewProps = {}; let collapsibleProps = {}; Object.keys(this.props).forEach((key) => { if (COLLAPSIBLE_PROPS.indexOf(key) !== -1) { collapsibleProps[key] = this.props[key]; } else if (VIEW_PROPS.indexOf(key) !== -1) { viewProps[key] = this.props[key]; } }); return ( <View {...viewProps}> {this.props.sections.map((section, key) => ( <View key={key}> <TouchableOpacity onPress={() => this._toggleSection(section)} underlayColor={this.props.underlayColor}> {this.props.renderHeader} </TouchableOpacity> <Collapsible collapsed={this.state.activeSection !== section} {...collapsibleProps}> {this.props.renderContent} </Collapsible> </View> ))} </View> ); } } export default Accordion; <Accordion sections={[rowData]} renderHeader={header} renderContent={content} />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment