Skip to content

Commit be0717c

Browse files
ethansharInbal-Tish
authored andcommitted
Adding carousel support to tab controller (#587)
* WIP - adding carousel support to tab controller * fix minor issue with tab changin * fix issue on Android with comparing values * minor refactor for when we use animated carousel * fix prop type * pass asCarousel prop once instead of passing asCarouselPage to each page * Use Animated.ScrollView instead of our Carousel
1 parent cdfb393 commit be0717c

File tree

4 files changed

+107
-22
lines changed

4 files changed

+107
-22
lines changed

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

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ import Tab1 from './tab1';
66
import Tab2 from './tab2';
77
import Tab3 from './tab3';
88

9-
const TABS = ['tab1', 'tab2' , 'tab3', 'account', 'groups', 'blog'];
9+
const USE_CAROUSEL = true;
10+
const TABS = ['tab1', 'tab2', 'tab3', 'account', 'groups', 'blog'];
1011

1112
class TabControllerScreen extends Component {
1213
state = {
1314
selectedIndex: 0,
1415
items: [
1516
..._.map(TABS, tab => ({label: tab, key: tab})),
16-
{key: 'addTabs', icon: Assets.icons.settings, ignore: true, onPress: this.addTab},
17+
{key: 'addTabs', icon: Assets.icons.settings, ignore: true, onPress: this.addTab}
1718
],
1819

1920
tabsCount: 3,
20-
key: Date.now(),
21+
key: Date.now()
2122
};
2223

2324
componentDidMount() {
@@ -44,11 +45,14 @@ class TabControllerScreen extends Component {
4445

4546
getItems = () => {
4647
const {tabsCount} = this.state;
47-
const items = _.chain(TABS).take(tabsCount).map(tab => ({label: tab, key: tab})).value();
48+
const items = _.chain(TABS)
49+
.take(tabsCount)
50+
.map(tab => ({label: tab, key: tab}))
51+
.value();
4852
items.push({key: 'addTabs', icon: Assets.icons.settings, ignore: true, onPress: this.addTab});
4953

5054
return items;
51-
}
55+
};
5256

5357
// renderTabItems() {
5458
// const {tabsCount} = this.state;
@@ -68,37 +72,40 @@ class TabControllerScreen extends Component {
6872
// }
6973

7074
renderTabPages() {
75+
const Container = USE_CAROUSEL ? Incubator.TabController.PageCarousel : View;
76+
const containerProps = USE_CAROUSEL ? {} : {flex: true};
7177
return (
72-
<View flex>
78+
<Container {...containerProps}>
7379
<Incubator.TabController.TabPage index={0}>
74-
<Tab1 />
80+
<Tab1/>
7581
</Incubator.TabController.TabPage>
76-
<Incubator.TabController.TabPage index={1} lazy>
77-
<Tab2 />
82+
<Incubator.TabController.TabPage index={1} lazy={!USE_CAROUSEL}>
83+
<Tab2/>
7884
</Incubator.TabController.TabPage>
7985
<Incubator.TabController.TabPage index={2}>
80-
<Tab3 />
86+
<Tab3/>
8187
</Incubator.TabController.TabPage>
82-
<Incubator.TabController.TabPage index={3}>
88+
{/* <Incubator.TabController.TabPage index={3}>
8389
<Text text40>ACCOUNT</Text>
8490
</Incubator.TabController.TabPage>
8591
<Incubator.TabController.TabPage index={4}>
8692
<Text text40>GROUPS</Text>
8793
</Incubator.TabController.TabPage>
8894
<Incubator.TabController.TabPage index={5}>
8995
<Text text40>BLOG</Text>
90-
</Incubator.TabController.TabPage>
91-
</View>
96+
</Incubator.TabController.TabPage> */}
97+
</Container>
9298
);
9399
}
94100

95101
render() {
96-
const {key, selectedIndex, items} = this.state;
102+
const {key, selectedIndex} = this.state;
97103
return (
98104
<View flex bg-dark80>
99105
<View flex>
100106
<Incubator.TabController
101107
key={key}
108+
asCarousel={USE_CAROUSEL}
102109
selectedIndex={selectedIndex}
103110
_onChangeIndex={index => console.warn('tab index is', index)}
104111
>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, {Component} from 'react';
2+
import TabBarContext from './TabBarContext';
3+
import Animated from 'react-native-reanimated';
4+
import {Constants} from '../../helpers';
5+
6+
const {Code, block, call} = Animated;
7+
8+
class PageCarousel extends Component {
9+
static contextType = TabBarContext;
10+
carousel = React.createRef();
11+
12+
onScroll = Animated.event([{nativeEvent: {contentOffset: {x: this.context.carouselOffset}}}], {
13+
useNativeDriver: true
14+
});
15+
16+
onTabChange = ([index]) => {
17+
const node = this.carousel.current.getNode();
18+
node.scrollTo({x: index * Constants.screenWidth, animated: true});
19+
};
20+
21+
render() {
22+
const {currentPage} = this.context;
23+
return (
24+
<>
25+
<Animated.ScrollView
26+
{...this.props}
27+
ref={this.carousel}
28+
horizontal
29+
pagingEnabled
30+
showsHorizontalScrollIndicator={false}
31+
onScroll={this.onScroll}
32+
scrollEventThrottle={200}
33+
/>
34+
35+
<Code>
36+
{() => {
37+
return block([call([currentPage], this.onTabChange)]);
38+
}}
39+
</Code>
40+
</>
41+
);
42+
}
43+
}
44+
45+
export default PageCarousel;

src/incubator/TabController/TabPage.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {StyleSheet} from 'react-native';
33
import PropTypes from 'prop-types';
44
import Reanimated from 'react-native-reanimated';
55
import TabBarContext from './TabBarContext';
6+
import {Constants} from '../../helpers';
67

78
const {Code, Value, cond, set, and, call, block, eq} = Reanimated;
89

@@ -36,7 +37,11 @@ export default class TabPage extends PureComponent {
3637

3738
_opacity = new Value(0);
3839
_zIndex = new Value(0);
39-
_pageStyle = [styles.page, {opacity: this._opacity}, {zIndex: this._zIndex}];
40+
_pageStyle = [
41+
{opacity: this._opacity},
42+
this.context.asCarousel ? styles.carouselPage : styles.page,
43+
{zIndex: this._zIndex}
44+
];
4045

4146
lazyLoad = () => {
4247
this.setState({
@@ -58,7 +63,7 @@ export default class TabPage extends PureComponent {
5863
cond(and(eq(currentPage, index), lazy, !loaded), call([], this.lazyLoad)),
5964
cond(eq(currentPage, index),
6065
[set(this._opacity, 1), set(this._zIndex, 1)],
61-
[set(this._opacity, 0), set(this._zIndex, 0)],)
66+
[set(this._opacity, 0), set(this._zIndex, 0)])
6267
]);
6368
}}
6469
</Code>
@@ -70,5 +75,10 @@ export default class TabPage extends PureComponent {
7075
const styles = StyleSheet.create({
7176
page: {
7277
...StyleSheet.absoluteFillObject
78+
},
79+
carouselPage: {
80+
width: Constants.screenWidth,
81+
flex: 1,
82+
opacity: 1
7383
}
7484
});

src/incubator/TabController/index.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import PropTypes from 'prop-types';
66
import _ from 'lodash';
77
import Reanimated from 'react-native-reanimated';
88
import {State} from 'react-native-gesture-handler';
9+
import {Constants} from '../../helpers';
910
import TabBarContext from './TabBarContext';
1011
import TabBar from './TabBar';
1112
import TabBarItem from './TabBarItem';
1213
import TabPage from './TabPage';
14+
import PageCarousel from './PageCarousel';
1315

14-
const {cond, Code, and, eq, set, Value, block} = Reanimated;
16+
const {cond, Code, and, eq, set, Value, block, round} = Reanimated;
1517

1618
/**
1719
* @description: A performant solution for a tab controller with lazy load mechanism
@@ -33,11 +35,15 @@ class TabController extends Component {
3335
/**
3436
* callback for when index has change (will not be called on ignored items)
3537
*/
36-
onChangeIndex: PropTypes.func
38+
onChangeIndex: PropTypes.func,
3739
// /**
3840
// * callback for when tab selected
3941
// */
4042
// onTabSelected: PropTypes.func,
43+
/**
44+
* When using TabController.PageCarousel this should be turned on
45+
*/
46+
asCarousel: PropTypes.bool
4147
};
4248

4349
static defaultProps = {
@@ -51,16 +57,19 @@ class TabController extends Component {
5157

5258
_targetPage = new Value(-1);
5359
_currentPage = new Value(this.props.selectedIndex);
60+
_carouselOffset = new Value(0);
5461

5562
getProviderContextValue = () => {
5663
const {itemStates} = this.state;
57-
const {onChangeIndex, selectedIndex} = this.props;
64+
const {onChangeIndex, selectedIndex, asCarousel} = this.props;
5865
return {
5966
selectedIndex,
6067
currentPage: this._currentPage,
68+
carouselOffset: this._carouselOffset,
6169
itemStates,
6270
registerTabItems: this.registerTabItems,
63-
onChangeIndex
71+
onChangeIndex,
72+
asCarousel
6473
};
6574
};
6675

@@ -71,18 +80,31 @@ class TabController extends Component {
7180

7281
render() {
7382
const {itemStates, ignoredItems} = this.state;
83+
84+
// Rounding on Android, cause it cause issues when comparing values
85+
const screenWidth = Constants.isAndroid ? Math.round(Constants.screenWidth) : Constants.screenWidth;
86+
7487
return (
7588
<TabBarContext.Provider value={this.getProviderContextValue()}>
7689
{this.props.children}
7790
{!_.isEmpty(itemStates) && (
7891
<Code>
7992
{() =>
8093
block([
94+
// Carousel Page change
95+
..._.times(itemStates.length, index => {
96+
return cond(eq(Constants.isAndroid ? round(this._carouselOffset) : this._carouselOffset,
97+
index * screenWidth),
98+
[set(this._currentPage, index)]);
99+
}),
100+
// TabBar Page change
81101
..._.map(itemStates, (state, index) => {
82102
return [
83103
cond(and(eq(state, State.BEGAN), !_.includes(ignoredItems, index)), set(this._targetPage, index)),
84-
cond(and(eq(this._targetPage, index), eq(state, State.END), !_.includes(ignoredItems, index)),
85-
set(this._currentPage, index),)
104+
cond(and(eq(this._targetPage, index), eq(state, State.END), !_.includes(ignoredItems, index)), [
105+
set(this._currentPage, index),
106+
set(this._targetPage, -1)
107+
])
86108
];
87109
})
88110
])
@@ -97,4 +119,5 @@ class TabController extends Component {
97119
TabController.TabBar = TabBar;
98120
TabController.TabBarItem = TabBarItem;
99121
TabController.TabPage = TabPage;
122+
TabController.PageCarousel = PageCarousel;
100123
export default TabController;

0 commit comments

Comments
 (0)