Skip to content

Commit bda35fb

Browse files
committed
hotfix for transition issues in TabController in reanimated >= v1.5.0
1 parent 214ab89 commit bda35fb

File tree

6 files changed

+270
-218
lines changed

6 files changed

+270
-218
lines changed

demo/src/screens/incubatorScreens/TabControllerScreen/index.js

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import Tab1 from './tab1';
77
import Tab2 from './tab2';
88
import Tab3 from './tab3';
99

10-
const USE_CAROUSEL = true;
1110
const TABS = ['Home', 'Me', 'Dashboard', 'account', 'groups', 'blog'];
1211

1312
class TabControllerScreen extends Component {
1413
state = {
14+
asCarousel: true,
1515
selectedIndex: 0,
1616
items: [
1717
..._.map(TABS, tab => ({label: tab, key: tab})),
@@ -72,6 +72,17 @@ class TabControllerScreen extends Component {
7272
// ];
7373
// }
7474

75+
toggleCarouselMode = () => {
76+
this.setState({
77+
asCarousel: !this.state.asCarousel,
78+
key: this.state.asCarousel ? 'asCarousel' : 'staticPages'
79+
});
80+
};
81+
82+
onChangeIndex = selectedIndex => {
83+
this.setState({selectedIndex});
84+
};
85+
7586
renderLoadingPage() {
7687
return (
7788
<View flex center>
@@ -84,8 +95,9 @@ class TabControllerScreen extends Component {
8495
}
8596

8697
renderTabPages() {
87-
const Container = USE_CAROUSEL ? Incubator.TabController.PageCarousel : View;
88-
const containerProps = USE_CAROUSEL ? {} : {flex: true};
98+
const {asCarousel} = this.state;
99+
const Container = asCarousel ? Incubator.TabController.PageCarousel : View;
100+
const containerProps = asCarousel ? {} : {flex: true};
89101
return (
90102
<Container {...containerProps}>
91103
<Incubator.TabController.TabPage index={0}>
@@ -111,33 +123,38 @@ class TabControllerScreen extends Component {
111123
}
112124

113125
render() {
114-
const {key, selectedIndex} = this.state;
126+
const {key, selectedIndex, asCarousel} = this.state;
115127
return (
116-
<View flex bg-dark80>
117-
<View flex>
118-
<Incubator.TabController
119-
key={key}
120-
asCarousel={USE_CAROUSEL}
121-
selectedIndex={selectedIndex}
122-
onChangeIndex={index => console.warn('tab index is', index)}
128+
<View flex bg-grey70>
129+
<Incubator.TabController
130+
key={key}
131+
asCarousel={asCarousel}
132+
selectedIndex={selectedIndex}
133+
onChangeIndex={this.onChangeIndex}
134+
>
135+
<Incubator.TabController.TabBar
136+
items={this.getItems()}
137+
// key={key}
138+
// uppercase
139+
// indicatorStyle={{backgroundColor: 'green', height: 3}}
140+
// labelColor={'green'}
141+
// selectedLabelColor={'red'}
142+
// labelStyle={{fontSize: 20}}
143+
// iconColor={'green'}
144+
// selectedIconColor={'blue'}
145+
activeBackgroundColor={Colors.blue60}
123146
>
124-
<Incubator.TabController.TabBar
125-
items={this.getItems()}
126-
// key={key}
127-
// uppercase
128-
// indicatorStyle={{backgroundColor: 'green', height: 3}}
129-
// labelColor={'green'}
130-
// selectedLabelColor={'red'}
131-
// labelStyle={{fontSize: 20}}
132-
// iconColor={'green'}
133-
// selectedIconColor={'blue'}
134-
activeBackgroundColor={Colors.blue60}
135-
>
136-
{/* {this.renderTabItems()} */}
137-
</Incubator.TabController.TabBar>
138-
{this.renderTabPages()}
139-
</Incubator.TabController>
140-
</View>
147+
{/* {this.renderTabItems()} */}
148+
</Incubator.TabController.TabBar>
149+
{this.renderTabPages()}
150+
</Incubator.TabController>
151+
<Button
152+
bg-grey20={!asCarousel}
153+
bg-green30={asCarousel}
154+
label={`Carousel:${asCarousel ? 'ON' : 'OFF'}`}
155+
style={{position: 'absolute', bottom: 100, right: 20}}
156+
onPress={this.toggleCarouselMode}
157+
/>
141158
</View>
142159
);
143160
}

src/incubator/TabController/PageCarousel.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React, {Component} from 'react';
1+
import React, {PureComponent} from 'react';
22
import TabBarContext from './TabBarContext';
33
import Animated from 'react-native-reanimated';
44
import {Constants} from '../../helpers';
55

66
const {Code, block, call} = Animated;
77

8-
class PageCarousel extends Component {
8+
class PageCarousel extends PureComponent {
99
static contextType = TabBarContext;
1010
carousel = React.createRef();
1111

@@ -28,10 +28,15 @@ class PageCarousel extends Component {
2828
scrollToPage = (pageIndex, animated) => {
2929
const node = this.carousel.current.getNode();
3030
node.scrollTo({x: pageIndex * Constants.screenWidth, animated});
31-
}
31+
};
32+
33+
renderCodeBlock = () => {
34+
const {currentPage} = this.context;
35+
return block([Animated.onChange(currentPage, call([currentPage], this.onTabChange))]);
36+
};
3237

3338
render() {
34-
const {selectedIndex, currentPage} = this.context;
39+
const {selectedIndex} = this.context;
3540
return (
3641
<>
3742
<Animated.ScrollView
@@ -45,13 +50,7 @@ class PageCarousel extends Component {
4550
contentOffset={{x: selectedIndex * Constants.screenWidth}} // iOS only
4651
/>
4752

48-
<Code>
49-
{() => {
50-
return block([
51-
Animated.onChange(currentPage, call([currentPage], this.onTabChange))
52-
]);
53-
}}
54-
</Code>
53+
<Code>{this.renderCodeBlock}</Code>
5554
</>
5655
);
5756
}

src/incubator/TabController/TabBar.js

Lines changed: 95 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import _ from 'lodash';
88

99
import TabBarContext from './TabBarContext';
1010
import TabBarItem from './TabBarItem';
11-
import ReanimatedObject from './ReanimatedObject';
11+
// import ReanimatedObject from './ReanimatedObject';
1212
import {asBaseComponent, forwardRef} from '../../commons';
1313
import View from '../../components/view';
1414
import Text from '../../components/text';
@@ -17,7 +17,23 @@ import {Constants} from '../../helpers';
1717
import {LogService} from '../../services';
1818

1919
const DEFAULT_HEIGHT = 48;
20-
const {Code, Clock, Value, add, sub, cond, eq, stopClock, startClock, clockRunning, timing, block, set} = Reanimated;
20+
const INDICATOR_INSET = Spacings.s4;
21+
const {
22+
Code,
23+
Clock,
24+
Value,
25+
and,
26+
eq,
27+
neq,
28+
cond,
29+
stopClock,
30+
startClock,
31+
interpolate,
32+
Extrapolate,
33+
timing,
34+
block,
35+
set
36+
} = Reanimated;
2137

2238
/**
2339
* @description: TabController's TabBar component
@@ -99,11 +115,12 @@ class TabBar extends PureComponent {
99115
this.tabBar = React.createRef();
100116

101117
this._itemsWidths = _.times(itemsCount, () => null);
102-
this._indicatorOffset = new ReanimatedObject({duration: 300, easing: Easing.bezier(0.23, 1, 0.32, 1)});
103-
this._indicatorWidth = new ReanimatedObject({duration: 300, easing: Easing.bezier(0.23, 1, 0.32, 1)});
118+
this._indicatorOffset = new Value(0);
119+
this._indicatorWidth = new Value(0);
120+
104121
this._indicatorTransitionStyle = {
105-
width: this._indicatorWidth.value,
106-
left: this._indicatorOffset.value
122+
width: this._indicatorWidth,
123+
left: this._indicatorOffset
107124
};
108125

109126
this.state = {
@@ -161,9 +178,16 @@ class TabBar extends PureComponent {
161178
this._itemsWidths[itemIndex] = itemWidth;
162179
if (!_.includes(this._itemsWidths, null)) {
163180
const {selectedIndex} = this.context;
164-
const itemsOffsets = _.map(this._itemsWidths, (w, index) => _.sum(_.take(this._itemsWidths, index)));
165-
this.setState({itemsWidths: this._itemsWidths, itemsOffsets});
166-
this.tabBar.current.scrollTo({x: itemsOffsets[selectedIndex], animated: false});
181+
const itemsOffsets = _.map(this._itemsWidths,
182+
(w, index) => INDICATOR_INSET + _.sum(_.take(this._itemsWidths, index)));
183+
const itemsWidths = _.map(this._itemsWidths, width => width - INDICATOR_INSET * 2);
184+
185+
this.setState({itemsWidths, itemsOffsets});
186+
const selectedItemOffset = itemsOffsets[selectedIndex] - INDICATOR_INSET;
187+
188+
if (selectedItemOffset + this._itemsWidths[selectedIndex] > Constants.screenWidth) {
189+
this.tabBar.current.scrollTo({x: selectedItemOffset, animated: true});
190+
}
167191
}
168192
};
169193

@@ -173,35 +197,6 @@ class TabBar extends PureComponent {
173197
}
174198
};
175199

176-
runTiming(targetValue, prevValue, duration) {
177-
const clock = new Clock();
178-
const state = {
179-
finished: new Value(0),
180-
position: prevValue,
181-
time: new Value(0),
182-
frameTime: new Value(0)
183-
};
184-
185-
const config = {
186-
duration,
187-
toValue: targetValue,
188-
easing: Easing.bezier(0.23, 1, 0.32, 1)
189-
};
190-
191-
return block([
192-
cond(clockRunning(clock), [], [startClock(clock)]),
193-
timing(clock, state, config),
194-
cond(state.finished, [
195-
stopClock(clock),
196-
set(state.finished, 0),
197-
set(state.time, 0),
198-
set(state.frameTime, 0),
199-
set(prevValue, state.position)
200-
]),
201-
state.position
202-
]);
203-
}
204-
205200
renderSelectedIndicator() {
206201
const {itemsWidths} = this.state;
207202
const {indicatorStyle} = this.props;
@@ -248,7 +243,6 @@ class TabBar extends PureComponent {
248243
});
249244
} else {
250245
// TODO: Remove once props.children is deprecated
251-
252246
if (this.tabBarItems) {
253247
return this.tabBarItems;
254248
}
@@ -273,10 +267,35 @@ class TabBar extends PureComponent {
273267
}
274268
}
275269

270+
renderCodeBlock = () => {
271+
const {carouselOffset, asCarousel} = this.context;
272+
const {itemsWidths, itemsOffsets} = this.state;
273+
const nodes = [];
274+
275+
if (asCarousel) {
276+
nodes.push(set(this._indicatorOffset,
277+
interpolate(carouselOffset, {
278+
inputRange: itemsOffsets.map((value, index) => index * Constants.screenWidth),
279+
outputRange: itemsOffsets,
280+
extrapolate: Extrapolate.CLAMP
281+
})),
282+
set(this._indicatorWidth,
283+
interpolate(carouselOffset, {
284+
inputRange: itemsWidths.map((value, index) => index * Constants.screenWidth),
285+
outputRange: itemsWidths,
286+
extrapolate: Extrapolate.CLAMP
287+
})));
288+
} else {
289+
nodes.push(set(this._indicatorOffset, runIndicatorTimer(new Clock(), this.context.currentPage, itemsOffsets)),
290+
set(this._indicatorWidth, runIndicatorTimer(new Clock(), this.context.currentPage, itemsWidths)));
291+
}
292+
293+
return block(nodes);
294+
};
295+
276296
render() {
277-
const {currentPage, carouselOffset, asCarousel} = this.context;
278297
const {height, enableShadow, containerStyle} = this.props;
279-
const {itemsWidths, itemsOffsets, scrollEnabled} = this.state;
298+
const {itemsWidths, scrollEnabled} = this.state;
280299
return (
281300
<View
282301
style={[styles.container, enableShadow && styles.containerShadow, {width: this.containerWidth}, containerStyle]}
@@ -293,48 +312,7 @@ class TabBar extends PureComponent {
293312
<View style={[styles.tabBar, height && {height}]}>{this.renderTabBarItems()}</View>
294313
{this.renderSelectedIndicator()}
295314
</ScrollView>
296-
{!_.isUndefined(itemsWidths) && (
297-
<Code>
298-
{() => {
299-
const indicatorInset = Spacings.s4;
300-
301-
return block(asCarousel && _.size(itemsWidths) > 1
302-
/* Transition for carousel pages */
303-
? [
304-
set(this._indicatorOffset.value,
305-
Reanimated.interpolate(carouselOffset, {
306-
inputRange: itemsOffsets.map((offset, index) => index * Constants.screenWidth),
307-
outputRange: itemsOffsets.map(offset => offset + indicatorInset)
308-
})),
309-
set(this._indicatorWidth.value,
310-
Reanimated.interpolate(carouselOffset, {
311-
inputRange: itemsWidths.map((width, index) => index * Constants.screenWidth),
312-
outputRange: itemsWidths.map((width, index) => width - 2 * indicatorInset)
313-
}))
314-
]
315-
/* Default transition */
316-
: [
317-
// calc indicator current width
318-
..._.map(itemsWidths, (width, index) => {
319-
return cond(eq(currentPage, index), [
320-
set(this._indicatorWidth.nextValue, sub(itemsWidths[index], indicatorInset * 2))
321-
]);
322-
}),
323-
// calc indicator current position
324-
..._.map(itemsOffsets, (offset, index) => {
325-
return cond(eq(currentPage, index), [
326-
set(this._indicatorOffset.nextValue, add(itemsOffsets[index], indicatorInset))
327-
]);
328-
}),
329-
330-
// Offset transition
331-
this._indicatorOffset.getTransitionBlock(),
332-
// Width transition
333-
this._indicatorWidth.getTransitionBlock()
334-
]);
335-
}}
336-
</Code>
337-
)}
315+
{_.size(itemsWidths) > 1 && <Code>{this.renderCodeBlock}</Code>}
338316
</View>
339317
);
340318
}
@@ -386,4 +364,38 @@ const styles = StyleSheet.create({
386364
}
387365
});
388366

367+
function runIndicatorTimer(clock, currentPage, values) {
368+
const state = {
369+
finished: new Value(0),
370+
position: new Value(0),
371+
time: new Value(0),
372+
frameTime: new Value(0)
373+
};
374+
375+
const config = {
376+
duration: 300,
377+
toValue: new Value(100),
378+
easing: Easing.inOut(Easing.ease)
379+
};
380+
381+
return block([
382+
..._.map(values, (value, index) => {
383+
return cond(and(eq(currentPage, index), neq(config.toValue, index)), [
384+
set(state.finished, 0),
385+
set(state.time, 0),
386+
set(state.frameTime, 0),
387+
set(config.toValue, index),
388+
startClock(clock)
389+
]);
390+
}),
391+
timing(clock, state, config),
392+
cond(state.finished, stopClock(clock)),
393+
interpolate(state.position, {
394+
inputRange: _.times(values.length),
395+
outputRange: values,
396+
extrapolate: Extrapolate.CLAMP
397+
})
398+
]);
399+
}
400+
389401
export default asBaseComponent(forwardRef(TabBar));

0 commit comments

Comments
 (0)