@@ -20,6 +20,9 @@ const SceneComponent = require('./SceneComponent');
2020const DefaultTabBar = require ( './DefaultTabBar' ) ;
2121const ScrollableTabBar = require ( './ScrollableTabBar' ) ;
2222
23+ const AnimatedViewPagerAndroid = Platform . OS === 'android' ?
24+ Animated . createAnimatedComponent ( ViewPagerAndroid ) :
25+ undefined ;
2326
2427const 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