@react-native-ohos/react-native-scrollable-tab-view 1.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +112 -0
  2. package/lib/commonjs/Button.android.js +17 -0
  3. package/lib/commonjs/Button.android.js.map +1 -0
  4. package/lib/commonjs/Button.ios.js +13 -0
  5. package/lib/commonjs/Button.ios.js.map +1 -0
  6. package/lib/commonjs/DefaultTabBar.js +113 -0
  7. package/lib/commonjs/DefaultTabBar.js.map +1 -0
  8. package/lib/commonjs/SceneComponent.js +23 -0
  9. package/lib/commonjs/SceneComponent.js.map +1 -0
  10. package/lib/commonjs/ScrollableTabBar.js +258 -0
  11. package/lib/commonjs/ScrollableTabBar.js.map +1 -0
  12. package/lib/commonjs/StaticContainer.js +17 -0
  13. package/lib/commonjs/StaticContainer.js.map +1 -0
  14. package/lib/commonjs/index.js +419 -0
  15. package/lib/commonjs/index.js.map +1 -0
  16. package/lib/commonjs/react-native-scrollable-tab-view.iml +11 -0
  17. package/lib/module/Button.android.js +15 -0
  18. package/lib/module/Button.android.js.map +1 -0
  19. package/lib/module/Button.ios.js +11 -0
  20. package/lib/module/Button.ios.js.map +1 -0
  21. package/lib/module/DefaultTabBar.js +111 -0
  22. package/lib/module/DefaultTabBar.js.map +1 -0
  23. package/lib/module/SceneComponent.js +21 -0
  24. package/lib/module/SceneComponent.js.map +1 -0
  25. package/lib/module/ScrollableTabBar.js +256 -0
  26. package/lib/module/ScrollableTabBar.js.map +1 -0
  27. package/lib/module/StaticContainer.js +15 -0
  28. package/lib/module/StaticContainer.js.map +1 -0
  29. package/lib/module/index.js +417 -0
  30. package/lib/module/index.js.map +1 -0
  31. package/lib/module/react-native-scrollable-tab-view.iml +11 -0
  32. package/package.json +69 -0
  33. package/src/Button.android.js +18 -0
  34. package/src/Button.ios.js +14 -0
  35. package/src/DefaultTabBar.js +117 -0
  36. package/src/SceneComponent.js +17 -0
  37. package/src/ScrollableTabBar.js +252 -0
  38. package/src/StaticContainer.js +19 -0
  39. package/src/index.js +417 -0
  40. package/src/react-native-scrollable-tab-view.iml +11 -0
@@ -0,0 +1,17 @@
1
+ const React = require('react');
2
+ const ReactNative = require('react-native');
3
+ const { Component } = React;
4
+ const { View, StyleSheet } = ReactNative;
5
+
6
+ const StaticContainer = require('./StaticContainer');
7
+
8
+ const SceneComponent = (Props) => {
9
+ const { shouldUpdated, ...props } = Props;
10
+ return <View {...props}>
11
+ <StaticContainer shouldUpdate={shouldUpdated}>
12
+ {props.children}
13
+ </StaticContainer>
14
+ </View>;
15
+ };
16
+
17
+ module.exports = SceneComponent;
@@ -0,0 +1,252 @@
1
+ const React = require('react');
2
+ const ReactNative = require('react-native');
3
+ const DeprecatedPropTypes = require('deprecated-react-native-prop-types');
4
+ const PropTypes = require('prop-types');
5
+ const createReactClass = require('create-react-class');
6
+ const {
7
+ View,
8
+ Animated,
9
+ StyleSheet,
10
+ ScrollView,
11
+ Text,
12
+ Platform,
13
+ Dimensions,
14
+ } = ReactNative;
15
+ const Button = Platform.OS === 'ios' ? require('./Button.ios.js') : require('./Button.android.js');
16
+
17
+ const WINDOW_WIDTH = Dimensions.get('window').width;
18
+
19
+ const ScrollableTabBar = createReactClass({
20
+ propTypes: {
21
+ goToPage: PropTypes.func,
22
+ activeTab: PropTypes.number,
23
+ tabs: PropTypes.array,
24
+ backgroundColor: PropTypes.string,
25
+ activeTextColor: PropTypes.string,
26
+ inactiveTextColor: PropTypes.string,
27
+ scrollOffset: PropTypes.number,
28
+ style: DeprecatedPropTypes.ViewPropTypes.style,
29
+ tabStyle: DeprecatedPropTypes.ViewPropTypes.style,
30
+ tabsContainerStyle: DeprecatedPropTypes.ViewPropTypes.style,
31
+ textStyle: DeprecatedPropTypes.TextPropTypes.style,
32
+ renderTab: PropTypes.func,
33
+ underlineStyle: DeprecatedPropTypes.ViewPropTypes.style,
34
+ onScroll: PropTypes.func,
35
+ },
36
+
37
+ getDefaultProps() {
38
+ return {
39
+ scrollOffset: 52,
40
+ activeTextColor: 'navy',
41
+ inactiveTextColor: 'black',
42
+ backgroundColor: null,
43
+ style: {},
44
+ tabStyle: {},
45
+ tabsContainerStyle: {},
46
+ underlineStyle: {},
47
+ };
48
+ },
49
+
50
+ getInitialState() {
51
+ this._tabsMeasurements = [];
52
+ return {
53
+ _leftTabUnderline: new Animated.Value(0),
54
+ _widthTabUnderline: new Animated.Value(0),
55
+ _containerWidth: null,
56
+ };
57
+ },
58
+
59
+ componentDidMount() {
60
+ this.props.scrollValue.addListener(this.updateView);
61
+ },
62
+
63
+ updateView(offset) {
64
+ const position = Math.floor(offset.value);
65
+ const pageOffset = offset.value % 1;
66
+ const tabCount = this.props.tabs.length;
67
+ const lastTabPosition = tabCount - 1;
68
+
69
+ if (tabCount === 0 || offset.value < 0 || offset.value > lastTabPosition) {
70
+ return;
71
+ }
72
+
73
+ if (this.necessarilyMeasurementsCompleted(position, position === lastTabPosition)) {
74
+ this.updateTabPanel(position, pageOffset);
75
+ this.updateTabUnderline(position, pageOffset, tabCount);
76
+ }
77
+ },
78
+
79
+ necessarilyMeasurementsCompleted(position, isLastTab) {
80
+ return this._tabsMeasurements[position] &&
81
+ (isLastTab || this._tabsMeasurements[position + 1]) &&
82
+ this._tabContainerMeasurements &&
83
+ this._containerMeasurements;
84
+ },
85
+
86
+ updateTabPanel(position, pageOffset) {
87
+ const containerWidth = this._containerMeasurements.width;
88
+ const tabWidth = this._tabsMeasurements[position].width;
89
+ const nextTabMeasurements = this._tabsMeasurements[position + 1];
90
+ const nextTabWidth = nextTabMeasurements && nextTabMeasurements.width || 0;
91
+ const tabOffset = this._tabsMeasurements[position].left;
92
+ const absolutePageOffset = pageOffset * tabWidth;
93
+ let newScrollX = tabOffset + absolutePageOffset;
94
+
95
+ // center tab and smooth tab change (for when tabWidth changes a lot between two tabs)
96
+ newScrollX -= (containerWidth - (1 - pageOffset) * tabWidth - pageOffset * nextTabWidth) / 2;
97
+ newScrollX = newScrollX >= 0 ? newScrollX : 0;
98
+
99
+ if (Platform.OS === 'android') {
100
+ const rightBoundScroll = this._tabContainerMeasurements.width - (this._containerMeasurements.width);
101
+ newScrollX = newScrollX > rightBoundScroll ? rightBoundScroll : newScrollX;
102
+ if(this._scrollView){
103
+ this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false, });
104
+ }
105
+ } else {
106
+ if(this._scrollView){
107
+ this._scrollView.scrollTo({x: newScrollX, y: 0, animated: false, });
108
+ }
109
+ }
110
+
111
+ },
112
+
113
+ updateTabUnderline(position, pageOffset, tabCount) {
114
+ const lineLeft = this._tabsMeasurements[position].left;
115
+ const lineRight = this._tabsMeasurements[position].right;
116
+
117
+ if (position < tabCount - 1) {
118
+ const nextTabLeft = this._tabsMeasurements[position + 1].left;
119
+ const nextTabRight = this._tabsMeasurements[position + 1].right;
120
+
121
+ const newLineLeft = (pageOffset * nextTabLeft + (1 - pageOffset) * lineLeft);
122
+ const newLineRight = (pageOffset * nextTabRight + (1 - pageOffset) * lineRight);
123
+
124
+ this.state._leftTabUnderline.setValue(newLineLeft);
125
+ this.state._widthTabUnderline.setValue(newLineRight - newLineLeft);
126
+ } else {
127
+ this.state._leftTabUnderline.setValue(lineLeft);
128
+ this.state._widthTabUnderline.setValue(lineRight - lineLeft);
129
+ }
130
+ },
131
+
132
+ renderTab(name, page, isTabActive, onPressHandler, onLayoutHandler) {
133
+ const { activeTextColor, inactiveTextColor, textStyle, } = this.props;
134
+ const textColor = isTabActive ? activeTextColor : inactiveTextColor;
135
+ const fontWeight = isTabActive ? 'bold' : 'normal';
136
+
137
+ return <Button
138
+ key={`${name}_${page}`}
139
+ accessible={true}
140
+ accessibilityLabel={name}
141
+ accessibilityTraits='button'
142
+ onPress={() => onPressHandler(page)}
143
+ onLayout={onLayoutHandler}
144
+ >
145
+ <View style={[styles.tab, this.props.tabStyle, ]}>
146
+ <Text style={[{color: textColor, fontWeight, }, textStyle, ]}>
147
+ {name}
148
+ </Text>
149
+ </View>
150
+ </Button>;
151
+ },
152
+
153
+ measureTab(page, event) {
154
+ const { x, width, height, } = event.nativeEvent.layout;
155
+ this._tabsMeasurements[page] = {left: x, right: x + width, width, height, };
156
+ this.updateView({value: this.props.scrollValue.__getValue(), });
157
+ },
158
+
159
+ render() {
160
+ const tabUnderlineStyle = {
161
+ position: 'absolute',
162
+ height: 4,
163
+ backgroundColor: 'navy',
164
+ bottom: 0,
165
+ };
166
+
167
+ const dynamicTabUnderline = {
168
+ left: this.state._leftTabUnderline,
169
+ width: this.state._widthTabUnderline,
170
+ };
171
+
172
+ const {
173
+ onScroll,
174
+ } = this.props;
175
+
176
+ return <View
177
+ style={[styles.container, {backgroundColor: this.props.backgroundColor, }, this.props.style, ]}
178
+ onLayout={this.onContainerLayout}
179
+ >
180
+ <ScrollView
181
+ ref={(scrollView) => { this._scrollView = scrollView; }}
182
+ horizontal={true}
183
+ showsHorizontalScrollIndicator={false}
184
+ showsVerticalScrollIndicator={false}
185
+ directionalLockEnabled={true}
186
+ bounces={false}
187
+ scrollsToTop={false}
188
+ onScroll={onScroll}
189
+ scrollEventThrottle={16}
190
+ >
191
+ <View
192
+ style={[styles.tabs, {width: this.state._containerWidth, }, this.props.tabsContainerStyle, ]}
193
+ ref={'tabContainer'}
194
+ onLayout={this.onTabContainerLayout}
195
+ >
196
+ {this.props.tabs.map((name, page) => {
197
+ const isTabActive = this.props.activeTab === page;
198
+ const renderTab = this.props.renderTab || this.renderTab;
199
+ return renderTab(name, page, isTabActive, this.props.goToPage, this.measureTab.bind(this, page));
200
+ })}
201
+ <Animated.View style={[tabUnderlineStyle, dynamicTabUnderline, this.props.underlineStyle, ]} />
202
+ </View>
203
+ </ScrollView>
204
+ </View>;
205
+ },
206
+
207
+ componentDidUpdate(prevProps) {
208
+ // If the tabs change, force the width of the tabs container to be recalculated
209
+ if (JSON.stringify(prevProps.tabs) !== JSON.stringify(this.props.tabs) && this.state._containerWidth) {
210
+ this.setState({ _containerWidth: null, });
211
+ }
212
+ },
213
+
214
+ onTabContainerLayout(e) {
215
+ this._tabContainerMeasurements = e.nativeEvent.layout;
216
+ let width = this._tabContainerMeasurements.width;
217
+ if (width < WINDOW_WIDTH) {
218
+ width = WINDOW_WIDTH;
219
+ }
220
+ this.setState({ _containerWidth: width, });
221
+ this.updateView({value: this.props.scrollValue.__getValue(), });
222
+ },
223
+
224
+ onContainerLayout(e) {
225
+ this._containerMeasurements = e.nativeEvent.layout;
226
+ this.updateView({value: this.props.scrollValue.__getValue(), });
227
+ },
228
+ });
229
+
230
+ module.exports = ScrollableTabBar;
231
+
232
+ const styles = StyleSheet.create({
233
+ tab: {
234
+ height: 49,
235
+ alignItems: 'center',
236
+ justifyContent: 'center',
237
+ paddingLeft: 20,
238
+ paddingRight: 20,
239
+ },
240
+ container: {
241
+ height: 50,
242
+ borderWidth: 1,
243
+ borderTopWidth: 0,
244
+ borderLeftWidth: 0,
245
+ borderRightWidth: 0,
246
+ borderColor: '#ccc',
247
+ },
248
+ tabs: {
249
+ flexDirection: 'row',
250
+ justifyContent: 'space-around',
251
+ },
252
+ });
@@ -0,0 +1,19 @@
1
+ const React = require('react');
2
+
3
+ class StaticContainer extends React.Component {
4
+
5
+ shouldComponentUpdate(nextProps: Object): boolean {
6
+ return !!nextProps.shouldUpdate;
7
+ }
8
+
9
+ render(): ?ReactElement {
10
+ var child = this.props.children;
11
+ if (child === null || child === false) {
12
+ return null;
13
+ }
14
+ return React.Children.only(child);
15
+ }
16
+
17
+ }
18
+
19
+ module.exports = StaticContainer;
package/src/index.js ADDED
@@ -0,0 +1,417 @@
1
+ import ViewPager from "react-native-pager-view";
2
+ import ViewPagerAndroid from "react-native-pager-view";
3
+
4
+ const React = require('react');
5
+ const ReactNative = require('react-native');
6
+ const DeprecatedPropTypes = require('deprecated-react-native-prop-types');
7
+ const createReactClass = require('create-react-class');
8
+ const PropTypes = require('prop-types');
9
+ const {
10
+ Dimensions,
11
+ View,
12
+ Animated,
13
+ ScrollView,
14
+ Platform,
15
+ StyleSheet,
16
+ InteractionManager,
17
+ } = ReactNative;
18
+
19
+ const TimerMixin = require('react-timer-mixin');
20
+
21
+ const SceneComponent = require('./SceneComponent');
22
+ const DefaultTabBar = require('./DefaultTabBar');
23
+ const ScrollableTabBar = require('./ScrollableTabBar');
24
+
25
+ const AnimatedViewPagerAndroid = Platform.OS === 'ios' ? undefined : Animated.createAnimatedComponent(ViewPager);
26
+
27
+ const ScrollableTabView = createReactClass({
28
+ mixins: [TimerMixin, ],
29
+ statics: {
30
+ DefaultTabBar,
31
+ ScrollableTabBar,
32
+ },
33
+ scrollOnMountCalled: false,
34
+ tabWillChangeWithoutGesture: false,
35
+
36
+ propTypes: {
37
+ tabBarPosition: PropTypes.oneOf(['top', 'bottom', 'overlayTop', 'overlayBottom', ]),
38
+ initialPage: PropTypes.number,
39
+ page: PropTypes.number,
40
+ onChangeTab: PropTypes.func,
41
+ onScroll: PropTypes.func,
42
+ renderTabBar: PropTypes.any,
43
+ tabBarUnderlineStyle: DeprecatedPropTypes.ViewPropTypes.style,
44
+ tabBarBackgroundColor: PropTypes.string,
45
+ tabBarActiveTextColor: PropTypes.string,
46
+ tabBarInactiveTextColor: PropTypes.string,
47
+ tabBarTextStyle: PropTypes.object,
48
+ style: DeprecatedPropTypes.ViewPropTypes.style,
49
+ contentProps: PropTypes.object,
50
+ scrollWithoutAnimation: PropTypes.bool,
51
+ locked: PropTypes.bool,
52
+ prerenderingSiblingsNumber: PropTypes.number,
53
+ },
54
+
55
+ getDefaultProps() {
56
+ return {
57
+ tabBarPosition: 'top',
58
+ initialPage: 0,
59
+ page: -1,
60
+ onChangeTab: () => {},
61
+ onScroll: () => {},
62
+ contentProps: {},
63
+ scrollWithoutAnimation: false,
64
+ locked: false,
65
+ prerenderingSiblingsNumber: 0,
66
+ };
67
+ },
68
+
69
+ getInitialState() {
70
+ const containerWidth = Dimensions.get('window').width;
71
+ let scrollValue;
72
+ let scrollXIOS;
73
+ let positionAndroid;
74
+ let offsetAndroid;
75
+
76
+ if (Platform.OS === 'ios') {
77
+ scrollXIOS = new Animated.Value(this.props.initialPage * containerWidth);
78
+ const containerWidthAnimatedValue = new Animated.Value(containerWidth);
79
+ // Need to call __makeNative manually to avoid a native animated bug. See
80
+ // https://github.com/facebook/react-native/pull/14435
81
+ containerWidthAnimatedValue.__makeNative();
82
+ scrollValue = Animated.divide(scrollXIOS, containerWidthAnimatedValue);
83
+
84
+ const callListeners = this._polyfillAnimatedValue(scrollValue);
85
+ scrollXIOS.addListener(
86
+ ({ value, }) => callListeners(value / this.state.containerWidth)
87
+ );
88
+ } else {
89
+ positionAndroid = new Animated.Value(this.props.initialPage);
90
+ offsetAndroid = new Animated.Value(0);
91
+ scrollValue = Animated.add(positionAndroid, offsetAndroid);
92
+
93
+ const callListeners = this._polyfillAnimatedValue(scrollValue);
94
+ let positionAndroidValue = this.props.initialPage;
95
+ let offsetAndroidValue = 0;
96
+ positionAndroid.addListener(({ value, }) => {
97
+ positionAndroidValue = value;
98
+ callListeners(positionAndroidValue + offsetAndroidValue);
99
+ });
100
+ offsetAndroid.addListener(({ value, }) => {
101
+ offsetAndroidValue = value;
102
+ callListeners(positionAndroidValue + offsetAndroidValue);
103
+ });
104
+ }
105
+
106
+ return {
107
+ currentPage: this.props.initialPage,
108
+ scrollValue,
109
+ scrollXIOS,
110
+ positionAndroid,
111
+ offsetAndroid,
112
+ containerWidth,
113
+ sceneKeys: this.newSceneKeys({ currentPage: this.props.initialPage, }),
114
+ };
115
+ },
116
+
117
+ componentDidUpdate(prevProps) {
118
+ if (this.props.children !== prevProps.children) {
119
+ this.updateSceneKeys({ page: this.state.currentPage, children: this.props.children, });
120
+ }
121
+
122
+ if (this.props.page >= 0 && this.props.page !== this.state.currentPage) {
123
+ this.goToPage(this.props.page);
124
+ }
125
+ },
126
+
127
+ componentWillUnmount() {
128
+ if (Platform.OS === 'ios') {
129
+ this.state.scrollXIOS.removeAllListeners();
130
+ } else {
131
+ this.state.positionAndroid.removeAllListeners();
132
+ this.state.offsetAndroid.removeAllListeners();
133
+ }
134
+ },
135
+
136
+ goToPage(pageNumber) {
137
+ if (Platform.OS === 'ios') {
138
+ const offset = pageNumber * this.state.containerWidth;
139
+ if (this.scrollView) {
140
+ this.scrollView.scrollTo({x: offset, y: 0, animated: !this.props.scrollWithoutAnimation, });
141
+ }
142
+ } else {
143
+ if (this.scrollView) {
144
+ this.tabWillChangeWithoutGesture = true;
145
+ if (this.props.scrollWithoutAnimation) {
146
+ this.scrollView.setPageWithoutAnimation(pageNumber);
147
+ } else {
148
+ this.scrollView.setPage(pageNumber);
149
+ }
150
+ }
151
+ }
152
+
153
+ const currentPage = this.state.currentPage;
154
+ this.updateSceneKeys({
155
+ page: pageNumber,
156
+ callback: this._onChangeTab.bind(this, currentPage, pageNumber),
157
+ });
158
+ },
159
+
160
+ renderTabBar(props) {
161
+ if (this.props.renderTabBar === false) {
162
+ return null;
163
+ } else if (this.props.renderTabBar) {
164
+ return React.cloneElement(this.props.renderTabBar(props), props);
165
+ } else {
166
+ return <DefaultTabBar {...props} />;
167
+ }
168
+ },
169
+
170
+ updateSceneKeys({ page, children = this.props.children, callback = () => {}, }) {
171
+ let newKeys = this.newSceneKeys({ previousKeys: this.state.sceneKeys, currentPage: page, children, });
172
+ this.setState({currentPage: page, sceneKeys: newKeys });
173
+ // fixed: 修复切换tab会闪烁的问题,callback中不依赖于最新的state,因此可以不用放在setState回调中
174
+ callback?.()
175
+ },
176
+
177
+ newSceneKeys({ previousKeys = [], currentPage = 0, children = this.props.children, }) {
178
+ let newKeys = [];
179
+ this._children(children).forEach((child, idx) => {
180
+ let key = this._makeSceneKey(child, idx);
181
+ if (this._keyExists(previousKeys, key) ||
182
+ this._shouldRenderSceneKey(idx, currentPage)) {
183
+ newKeys.push(key);
184
+ }
185
+ });
186
+ return newKeys;
187
+ },
188
+
189
+ // Animated.add and Animated.divide do not currently support listeners so
190
+ // we have to polyfill it here since a lot of code depends on being able
191
+ // to add a listener to `scrollValue`. See https://github.com/facebook/react-native/pull/12620.
192
+ _polyfillAnimatedValue(animatedValue) {
193
+
194
+ const listeners = new Set();
195
+ const addListener = (listener) => {
196
+ listeners.add(listener);
197
+ };
198
+
199
+ const removeListener = (listener) => {
200
+ listeners.delete(listener);
201
+ };
202
+
203
+ const removeAllListeners = () => {
204
+ listeners.clear();
205
+ };
206
+
207
+ animatedValue.addListener = addListener;
208
+ animatedValue.removeListener = removeListener;
209
+ animatedValue.removeAllListeners = removeAllListeners;
210
+
211
+ return (value) => listeners.forEach(listener => listener({ value, }));
212
+ },
213
+
214
+ _shouldRenderSceneKey(idx, currentPageKey) {
215
+ let numOfSibling = this.props.prerenderingSiblingsNumber;
216
+ return (idx < (currentPageKey + numOfSibling + 1) &&
217
+ idx > (currentPageKey - numOfSibling - 1));
218
+ },
219
+
220
+ _keyExists(sceneKeys, key) {
221
+ return sceneKeys.find((sceneKey) => key === sceneKey);
222
+ },
223
+
224
+ _makeSceneKey(child, idx) {
225
+ return child.props.tabLabel + '_' + idx;
226
+ },
227
+
228
+ renderScrollableContent() {
229
+ if (Platform.OS === 'ios') {
230
+ const scenes = this._composeScenes();
231
+ return <Animated.ScrollView
232
+ horizontal
233
+ pagingEnabled
234
+ automaticallyAdjustContentInsets={false}
235
+ contentOffset={{ x: this.props.initialPage * this.state.containerWidth, }}
236
+ ref={(scrollView) => { this.scrollView = scrollView; }}
237
+ onScroll={Animated.event(
238
+ [{ nativeEvent: { contentOffset: { x: this.state.scrollXIOS, }, }, }, ],
239
+ { useNativeDriver: false, listener: this._onScroll, }
240
+ )}
241
+ onMomentumScrollBegin={this._onMomentumScrollBeginAndEnd}
242
+ onMomentumScrollEnd={this._onMomentumScrollBeginAndEnd}
243
+ scrollEventThrottle={16}
244
+ scrollsToTop={false}
245
+ showsHorizontalScrollIndicator={false}
246
+ scrollEnabled={!this.props.locked}
247
+ directionalLockEnabled
248
+ alwaysBounceVertical={false}
249
+ keyboardDismissMode="on-drag"
250
+ {...this.props.contentProps}
251
+ >
252
+ {scenes}
253
+ </Animated.ScrollView>;
254
+ } else {
255
+ const scenes = this._composeScenes();
256
+ return <AnimatedViewPagerAndroid
257
+ key={this._children().length}
258
+ style={styles.scrollableContentAndroid}
259
+ initialPage={this.props.initialPage}
260
+ onPageSelected={this._updateSelectedPage}
261
+ keyboardDismissMode="on-drag"
262
+ scrollEnabled={!this.props.locked}
263
+ onPageScroll={Animated.event(
264
+ [{
265
+ nativeEvent: {
266
+ position: this.state.positionAndroid,
267
+ offset: this.state.offsetAndroid,
268
+ },
269
+ }, ],
270
+ {
271
+ useNativeDriver: false,
272
+ listener: this._onScroll,
273
+ },
274
+ )}
275
+ ref={(scrollView) => { this.scrollView = scrollView; }}
276
+ {...this.props.contentProps}
277
+ >
278
+ {scenes}
279
+ </AnimatedViewPagerAndroid>;
280
+ }
281
+ },
282
+
283
+ _composeScenes() {
284
+ return this._children().map((child, idx) => {
285
+ let key = this._makeSceneKey(child, idx);
286
+ return <SceneComponent
287
+ key={child.key}
288
+ shouldUpdated={this._shouldRenderSceneKey(idx, this.state.currentPage)}
289
+ style={{width: this.state.containerWidth, }}
290
+ >
291
+ {this._keyExists(this.state.sceneKeys, key) ? child : <View tabLabel={child.props.tabLabel}/>}
292
+ </SceneComponent>;
293
+ });
294
+ },
295
+
296
+ _onMomentumScrollBeginAndEnd(e) {
297
+ const offsetX = e.nativeEvent.contentOffset.x;
298
+ const page = Math.round(offsetX / this.state.containerWidth);
299
+ if (this.state.currentPage !== page) {
300
+ this._updateSelectedPage(page);
301
+ }
302
+ },
303
+
304
+ _updateSelectedPage(nextPage) {
305
+ let localNextPage = nextPage;
306
+ if (typeof localNextPage === 'object') {
307
+ localNextPage = nextPage.nativeEvent.position;
308
+ }
309
+
310
+ const currentPage = this.state.currentPage;
311
+ !this.tabWillChangeWithoutGesture && this.updateSceneKeys({
312
+ page: localNextPage,
313
+ callback: this._onChangeTab.bind(this, currentPage, localNextPage),
314
+ });
315
+ this.tabWillChangeWithoutGesture = false;
316
+ },
317
+
318
+ _onChangeTab(prevPage, currentPage) {
319
+ this.props.onChangeTab({
320
+ i: currentPage,
321
+ ref: this._children()[currentPage],
322
+ from: prevPage,
323
+ });
324
+ },
325
+
326
+ _onScroll(e) {
327
+ if (Platform.OS === 'ios') {
328
+ const offsetX = e.nativeEvent.contentOffset.x;
329
+ if (offsetX === 0 && !this.scrollOnMountCalled) {
330
+ this.scrollOnMountCalled = true;
331
+ } else {
332
+ this.props.onScroll(offsetX / this.state.containerWidth);
333
+ }
334
+ } else {
335
+ const { position, offset, } = e.nativeEvent;
336
+ this.props.onScroll(position + offset);
337
+ }
338
+ },
339
+
340
+ _handleLayout(e) {
341
+ const { width, } = e.nativeEvent.layout;
342
+
343
+ if (!width || width <= 0 || Math.round(width) === Math.round(this.state.containerWidth)) {
344
+ return;
345
+ }
346
+
347
+ if (Platform.OS === 'ios') {
348
+ const containerWidthAnimatedValue = new Animated.Value(width);
349
+ // Need to call __makeNative manually to avoid a native animated bug. See
350
+ // https://github.com/facebook/react-native/pull/14435
351
+ containerWidthAnimatedValue.__makeNative();
352
+ scrollValue = Animated.divide(this.state.scrollXIOS, containerWidthAnimatedValue);
353
+ this.setState({ containerWidth: width, scrollValue, });
354
+ } else {
355
+ this.setState({ containerWidth: width, });
356
+ }
357
+ this.requestAnimationFrame(() => {
358
+ this.goToPage(this.state.currentPage);
359
+ });
360
+ },
361
+
362
+ _children(children = this.props.children) {
363
+ return React.Children.map(children, (child) => child);
364
+ },
365
+
366
+ render() {
367
+ let overlayTabs = (this.props.tabBarPosition === 'overlayTop' || this.props.tabBarPosition === 'overlayBottom');
368
+ let tabBarProps = {
369
+ goToPage: this.goToPage,
370
+ tabs: this._children().map((child) => child.props.tabLabel),
371
+ activeTab: this.state.currentPage,
372
+ scrollValue: this.state.scrollValue,
373
+ containerWidth: this.state.containerWidth,
374
+ };
375
+
376
+ if (this.props.tabBarBackgroundColor) {
377
+ tabBarProps.backgroundColor = this.props.tabBarBackgroundColor;
378
+ }
379
+ if (this.props.tabBarActiveTextColor) {
380
+ tabBarProps.activeTextColor = this.props.tabBarActiveTextColor;
381
+ }
382
+ if (this.props.tabBarInactiveTextColor) {
383
+ tabBarProps.inactiveTextColor = this.props.tabBarInactiveTextColor;
384
+ }
385
+ if (this.props.tabBarTextStyle) {
386
+ tabBarProps.textStyle = this.props.tabBarTextStyle;
387
+ }
388
+ if (this.props.tabBarUnderlineStyle) {
389
+ tabBarProps.underlineStyle = this.props.tabBarUnderlineStyle;
390
+ }
391
+ if (overlayTabs) {
392
+ tabBarProps.style = {
393
+ position: 'absolute',
394
+ left: 0,
395
+ right: 0,
396
+ [this.props.tabBarPosition === 'overlayTop' ? 'top' : 'bottom']: 0,
397
+ };
398
+ }
399
+
400
+ return <View style={[styles.container, this.props.style, ]} onLayout={this._handleLayout}>
401
+ {this.props.tabBarPosition === 'top' && this.renderTabBar(tabBarProps)}
402
+ {this.renderScrollableContent()}
403
+ {(this.props.tabBarPosition === 'bottom' || overlayTabs) && this.renderTabBar(tabBarProps)}
404
+ </View>;
405
+ },
406
+ });
407
+
408
+ module.exports = ScrollableTabView;
409
+
410
+ const styles = StyleSheet.create({
411
+ container: {
412
+ flex: 1,
413
+ },
414
+ scrollableContentAndroid: {
415
+ flex: 1,
416
+ },
417
+ });