UNPKG

5.7 kBJavaScriptView Raw
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { Animated, Easing, View } from 'react-native';
4import { ViewPropTypes } from './config';
5
6const ANIMATED_EASING_PREFIXES = ['easeInOut', 'easeOut', 'easeIn'];
7
8export default class Collapsible extends Component {
9 static propTypes = {
10 align: PropTypes.oneOf(['top', 'center', 'bottom']),
11 collapsed: PropTypes.bool,
12 collapsedHeight: PropTypes.number,
13 duration: PropTypes.number,
14 easing: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
15 style: ViewPropTypes.style,
16 children: PropTypes.node,
17 };
18
19 static defaultProps = {
20 align: 'top',
21 collapsed: true,
22 collapsedHeight: 0,
23 duration: 300,
24 easing: 'easeOutCubic',
25 };
26
27 constructor(props) {
28 super(props);
29 this.state = {
30 measuring: false,
31 measured: false,
32 height: new Animated.Value(props.collapsedHeight),
33 contentHeight: 0,
34 animating: false,
35 };
36 }
37
38 componentWillReceiveProps(nextProps) {
39 if (!nextProps.collapsed && !this.props.collapsed) {
40 this.setState({ measured: false }, () =>
41 this._componentWillReceiveProps(nextProps)
42 );
43 } else {
44 this._componentWillReceiveProps(nextProps);
45 }
46 }
47
48 _componentWillReceiveProps(nextProps) {
49 if (nextProps.collapsed !== this.props.collapsed) {
50 this._toggleCollapsed(nextProps.collapsed);
51 } else if (
52 nextProps.collapsed &&
53 nextProps.collapsedHeight !== this.props.collapsedHeight
54 ) {
55 this.state.height.setValue(nextProps.collapsedHeight);
56 }
57 }
58
59 contentHandle = null;
60
61 _handleRef = ref => {
62 this.contentHandle = ref;
63 };
64
65 _measureContent(callback) {
66 this.setState(
67 {
68 measuring: true,
69 },
70 () => {
71 requestAnimationFrame(() => {
72 if (!this.contentHandle) {
73 this.setState(
74 {
75 measuring: false,
76 },
77 () => callback(this.props.collapsedHeight)
78 );
79 } else {
80 this.contentHandle.getNode().measure((x, y, width, height) => {
81 this.setState(
82 {
83 measuring: false,
84 measured: true,
85 contentHeight: height,
86 },
87 () => callback(height)
88 );
89 });
90 }
91 });
92 }
93 );
94 }
95
96 _toggleCollapsed(collapsed) {
97 if (collapsed) {
98 this._transitionToHeight(this.props.collapsedHeight);
99 } else if (!this.contentHandle) {
100 if (this.state.measured) {
101 this._transitionToHeight(this.state.contentHeight);
102 }
103 return;
104 } else {
105 this._measureContent(contentHeight => {
106 this._transitionToHeight(contentHeight);
107 });
108 }
109 }
110
111 _transitionToHeight(height) {
112 const { duration } = this.props;
113 let easing = this.props.easing;
114 if (typeof easing === 'string') {
115 let prefix;
116 let found = false;
117 for (let i = 0; i < ANIMATED_EASING_PREFIXES.length; i++) {
118 prefix = ANIMATED_EASING_PREFIXES[i];
119 if (easing.substr(0, prefix.length) === prefix) {
120 easing =
121 easing.substr(prefix.length, 1).toLowerCase() +
122 easing.substr(prefix.length + 1);
123 prefix = prefix.substr(4, 1).toLowerCase() + prefix.substr(5);
124 easing = Easing[prefix](Easing[easing || 'ease']);
125 found = true;
126 break;
127 }
128 }
129 if (!found) {
130 easing = Easing[easing];
131 }
132 if (!easing) {
133 throw new Error('Invalid easing type "' + this.props.easing + '"');
134 }
135 }
136
137 if (this._animation) {
138 this._animation.stop();
139 }
140 this.setState({ animating: true });
141 this._animation = Animated.timing(this.state.height, {
142 toValue: height,
143 duration,
144 easing,
145 }).start(() => this.setState({ animating: false }));
146 }
147
148 _handleLayoutChange = event => {
149 const contentHeight = event.nativeEvent.layout.height;
150 if (
151 this.state.animating ||
152 this.props.collapsed ||
153 this.state.measuring ||
154 this.state.contentHeight === contentHeight
155 ) {
156 return;
157 }
158
159 this.state.height.setValue(contentHeight);
160 this.setState({ contentHeight });
161 };
162
163 render() {
164 const { collapsed } = this.props;
165 const { height, contentHeight, measuring, measured } = this.state;
166 const hasKnownHeight = !measuring && (measured || collapsed);
167 const style = hasKnownHeight && {
168 overflow: 'hidden',
169 height: height,
170 };
171 const contentStyle = {};
172 if (measuring) {
173 contentStyle.position = 'absolute';
174 contentStyle.opacity = 0;
175 } else if (this.props.align === 'center') {
176 contentStyle.transform = [
177 {
178 translateY: height.interpolate({
179 inputRange: [0, contentHeight],
180 outputRange: [contentHeight / -2, 0],
181 }),
182 },
183 ];
184 } else if (this.props.align === 'bottom') {
185 contentStyle.transform = [
186 {
187 translateY: height.interpolate({
188 inputRange: [0, contentHeight],
189 outputRange: [-contentHeight, 0],
190 }),
191 },
192 ];
193 }
194 return (
195 <Animated.View style={style} pointerEvents={collapsed ? 'none' : 'auto'}>
196 <Animated.View
197 ref={this._handleRef}
198 style={[this.props.style, contentStyle]}
199 onLayout={this.state.animating ? undefined : this._handleLayoutChange}
200 >
201 <View style={{ height: measured ? contentHeight : null }}>
202 {this.props.children}
203 </View>
204 </Animated.View>
205 </Animated.View>
206 );
207 }
208}