Skip to content

Commit 1d3f424

Browse files
authored
Merge pull request #661 from janicduplessis/viewpager
Use native animations for DefaultTabBar
2 parents d63c5d4 + 8bd9f03 commit 1d3f424

File tree

2 files changed

+137
-28
lines changed

2 files changed

+137
-28
lines changed

DefaultTabBar.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ const DefaultTabBar = React.createClass({
6565
bottom: 0,
6666
};
6767

68-
const left = this.props.scrollValue.interpolate({
69-
inputRange: [0, 1, ], outputRange: [0, containerWidth / numberOfTabs, ],
68+
const translateX = this.props.scrollValue.interpolate({
69+
inputRange: [0, 1],
70+
outputRange: [0, containerWidth / numberOfTabs],
7071
});
7172
return (
7273
<View style={[styles.tabs, {backgroundColor: this.props.backgroundColor, }, this.props.style, ]}>
@@ -75,7 +76,17 @@ const DefaultTabBar = React.createClass({
7576
const renderTab = this.props.renderTab || this.renderTab;
7677
return renderTab(name, page, isTabActive, this.props.goToPage);
7778
})}
78-
<Animated.View style={[tabUnderlineStyle, { left, }, this.props.underlineStyle, ]} />
79+
<Animated.View
80+
style={[
81+
tabUnderlineStyle,
82+
{
83+
transform: [
84+
{ translateX },
85+
]
86+
},
87+
this.props.underlineStyle,
88+
]}
89+
/>
7990
</View>
8091
);
8192
},

index.js

Lines changed: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const SceneComponent = require('./SceneComponent');
2020
const DefaultTabBar = require('./DefaultTabBar');
2121
const ScrollableTabBar = require('./ScrollableTabBar');
2222

23+
const AnimatedViewPagerAndroid = Platform.OS === 'android' ?
24+
Animated.createAnimatedComponent(ViewPagerAndroid) :
25+
undefined;
2326

2427
const ScrollableTabView = React.createClass({
2528
mixins: [TimerMixin, ],
@@ -58,10 +61,49 @@ const ScrollableTabView = React.createClass({
5861
},
5962

6063
getInitialState() {
64+
const containerWidth = Dimensions.get('window').width;
65+
let scrollValue;
66+
let scrollXIOS;
67+
let positionAndroid;
68+
let offsetAndroid;
69+
70+
if (Platform.OS === 'ios') {
71+
scrollXIOS = new Animated.Value(this.props.initialPage * containerWidth);
72+
const containerWidthAnimatedValue = new Animated.Value(containerWidth);
73+
// Need to call __makeNative manually to avoid a native animated bug. See
74+
// https://github.com/facebook/react-native/pull/14435
75+
containerWidthAnimatedValue.__makeNative();
76+
scrollValue = Animated.divide(scrollXIOS, containerWidthAnimatedValue);
77+
78+
const callListeners = this._polyfillAnimatedValue(scrollValue);
79+
scrollXIOS.addListener(
80+
({ value, }) => callListeners(value / this.state.containerWidth)
81+
);
82+
} else {
83+
positionAndroid = new Animated.Value(this.props.initialPage);
84+
offsetAndroid = new Animated.Value(0);
85+
scrollValue = Animated.add(positionAndroid, offsetAndroid);
86+
87+
const callListeners = this._polyfillAnimatedValue(scrollValue);
88+
let positionAndroidValue = this.props.initialPage;
89+
let offsetAndroidValue = 0;
90+
positionAndroid.addListener(({ value, }) => {
91+
positionAndroidValue = value;
92+
callListeners(positionAndroidValue + offsetAndroidValue);
93+
});
94+
offsetAndroid.addListener(({ value, }) => {
95+
offsetAndroidValue = value;
96+
callListeners(positionAndroidValue + offsetAndroidValue);
97+
});
98+
}
99+
61100
return {
62101
currentPage: this.props.initialPage,
63-
scrollValue: new Animated.Value(this.props.initialPage),
64-
containerWidth: Dimensions.get('window').width,
102+
scrollValue,
103+
scrollXIOS,
104+
positionAndroid,
105+
offsetAndroid,
106+
containerWidth,
65107
sceneKeys: this.newSceneKeys({ currentPage: this.props.initialPage, }),
66108
};
67109
},
@@ -76,18 +118,27 @@ const ScrollableTabView = React.createClass({
76118
}
77119
},
78120

121+
componentWillUnmount() {
122+
if (Platform.OS === 'ios') {
123+
this.state.scrollXIOS.removeAllListeners();
124+
} else {
125+
this.state.positionAndroid.removeAllListeners();
126+
this.state.offsetAndroid.removeAllListeners();
127+
}
128+
},
129+
79130
goToPage(pageNumber) {
80131
if (Platform.OS === 'ios') {
81132
const offset = pageNumber * this.state.containerWidth;
82133
if (this.scrollView) {
83-
this.scrollView.scrollTo({x: offset, y: 0, animated: !this.props.scrollWithoutAnimation, });
134+
this.scrollView.getNode().scrollTo({x: offset, y: 0, animated: !this.props.scrollWithoutAnimation, });
84135
}
85136
} else {
86137
if (this.scrollView) {
87138
if (this.props.scrollWithoutAnimation) {
88-
this.scrollView.setPageWithoutAnimation(pageNumber);
139+
this.scrollView.getNode().setPageWithoutAnimation(pageNumber);
89140
} else {
90-
this.scrollView.setPage(pageNumber);
141+
this.scrollView.getNode().setPage(pageNumber);
91142
}
92143
}
93144
}
@@ -126,6 +177,31 @@ const ScrollableTabView = React.createClass({
126177
return newKeys;
127178
},
128179

180+
// Animated.add and Animated.divide do not currently support listeners so
181+
// we have to polyfill it here since a lot of code depends on being able
182+
// to add a listener to `scrollValue`. See https://github.com/facebook/react-native/pull/12620.
183+
_polyfillAnimatedValue(animatedValue) {
184+
185+
const listeners = new Set();
186+
const addListener = (listener) => {
187+
listeners.add(listener);
188+
};
189+
190+
const removeListener = (listener) => {
191+
listeners.delete(listener);
192+
};
193+
194+
const removeAllListeners = () => {
195+
listeners.clear();
196+
};
197+
198+
animatedValue.addListener = addListener;
199+
animatedValue.removeListener = removeListener;
200+
animatedValue.removeAllListeners = removeAllListeners;
201+
202+
return (value) => listeners.forEach(listener => listener({ value, }));
203+
},
204+
129205
_shouldRenderSceneKey(idx, currentPageKey) {
130206
let numOfSibling = this.props.prerenderingSiblingsNumber;
131207
return (idx < (currentPageKey + numOfSibling + 1) &&
@@ -143,20 +219,16 @@ const ScrollableTabView = React.createClass({
143219
renderScrollableContent() {
144220
if (Platform.OS === 'ios') {
145221
const scenes = this._composeScenes();
146-
return <ScrollView
222+
return <Animated.ScrollView
147223
horizontal
148224
pagingEnabled
149225
automaticallyAdjustContentInsets={false}
150226
contentOffset={{ x: this.props.initialPage * this.state.containerWidth, }}
151227
ref={(scrollView) => { this.scrollView = scrollView; }}
152-
onScroll={(e) => {
153-
const offsetX = e.nativeEvent.contentOffset.x;
154-
if (offsetX === 0 && !this.scrollOnMountCalled) {
155-
this.scrollOnMountCalled = true;
156-
} else {
157-
this._updateScrollValue(offsetX / this.state.containerWidth);
158-
}
159-
}}
228+
onScroll={Animated.event(
229+
[{ nativeEvent: { contentOffset: { x: this.state.scrollXIOS, }, }, }, ],
230+
{ useNativeDriver: true, listener: this._onScroll, }
231+
)}
160232
onMomentumScrollBegin={this._onMomentumScrollBeginAndEnd}
161233
onMomentumScrollEnd={this._onMomentumScrollBeginAndEnd}
162234
scrollEventThrottle={16}
@@ -169,25 +241,33 @@ const ScrollableTabView = React.createClass({
169241
{...this.props.contentProps}
170242
>
171243
{scenes}
172-
</ScrollView>;
244+
</Animated.ScrollView>;
173245
} else {
174246
const scenes = this._composeScenes();
175-
return <ViewPagerAndroid
247+
return <AnimatedViewPagerAndroid
176248
key={this._children().length}
177249
style={styles.scrollableContentAndroid}
178250
initialPage={this.props.initialPage}
179251
onPageSelected={this._updateSelectedPage}
180252
keyboardDismissMode="on-drag"
181253
scrollEnabled={!this.props.locked}
182-
onPageScroll={(e) => {
183-
const { offset, position, } = e.nativeEvent;
184-
this._updateScrollValue(position + offset);
185-
}}
254+
onPageScroll={Animated.event(
255+
[{
256+
nativeEvent: {
257+
position: this.state.positionAndroid,
258+
offset: this.state.offsetAndroid,
259+
},
260+
}, ],
261+
{
262+
useNativeDriver: true,
263+
listener: this._onScroll,
264+
},
265+
)}
186266
ref={(scrollView) => { this.scrollView = scrollView; }}
187267
{...this.props.contentProps}
188268
>
189269
{scenes}
190-
</ViewPagerAndroid>;
270+
</AnimatedViewPagerAndroid>;
191271
}
192272
},
193273

@@ -233,16 +313,34 @@ const ScrollableTabView = React.createClass({
233313
});
234314
},
235315

236-
_updateScrollValue(value) {
237-
this.state.scrollValue.setValue(value);
238-
this.props.onScroll(value);
316+
_onScroll(e) {
317+
if (Platform.OS === 'ios') {
318+
const offsetX = e.nativeEvent.contentOffset.x;
319+
if (offsetX === 0 && !this.scrollOnMountCalled) {
320+
this.scrollOnMountCalled = true;
321+
} else {
322+
this.props.onScroll(offsetX / this.state.containerWidth);
323+
}
324+
} else {
325+
const { position, offset, } = e.nativeEvent;
326+
this.props.onScroll(position + offset);
327+
}
239328
},
240329

241330
_handleLayout(e) {
242331
const { width, } = e.nativeEvent.layout;
243332

244333
if (Math.round(width) !== Math.round(this.state.containerWidth)) {
245-
this.setState({ containerWidth: width, });
334+
if (Platform.OS === 'ios') {
335+
const containerWidthAnimatedValue = new Animated.Value(width);
336+
// Need to call __makeNative manually to avoid a native animated bug. See
337+
// https://github.com/facebook/react-native/pull/14435
338+
containerWidthAnimatedValue.__makeNative();
339+
scrollValue = Animated.divide(this.state.scrollXIOS, containerWidthAnimatedValue);
340+
this.setState({ containerWidth: width, scrollValue, });
341+
} else {
342+
this.setState({ containerWidth: width, });
343+
}
246344
this.requestAnimationFrame(() => {
247345
this.goToPage(this.state.currentPage);
248346
});

0 commit comments

Comments
 (0)