Skip to content

Commit bfd2a70

Browse files
authored
Refactor/tab controller (#739)
* refactor tab controller using react-native-redash library * support centering selected item in TabController.TabBar * update TabController example screen * minor fix in how we pass style * fix initial index different than zero
1 parent 6b2dfaf commit bfd2a70

File tree

9 files changed

+160
-145
lines changed

9 files changed

+160
-145
lines changed

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

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ const TABS = ['Home', 'Posts', 'Reviews', 'Videos', 'Photos', 'Events', 'About',
1313
class TabControllerScreen extends Component {
1414
state = {
1515
asCarousel: true,
16+
centerSelected: false,
1617
selectedIndex: 0,
1718
items: _.chain(TABS)
18-
.map(tab => ({label: tab, key: tab}))
19+
.map((tab) => ({label: tab, key: tab}))
1920
.value(),
2021
key: Date.now()
2122
};
@@ -42,17 +43,21 @@ class TabControllerScreen extends Component {
4243
}
4344
};
4445

45-
46-
47-
4846
toggleCarouselMode = () => {
4947
this.setState({
5048
asCarousel: !this.state.asCarousel,
5149
key: this.state.asCarousel ? 'asCarousel' : 'staticPages'
5250
});
5351
};
5452

55-
onChangeIndex = selectedIndex => {
53+
toggleCenterSelected = () => {
54+
this.setState({
55+
centerSelected: !this.state.centerSelected,
56+
key: Date.now()
57+
});
58+
};
59+
60+
onChangeIndex = (selectedIndex) => {
5661
this.setState({selectedIndex});
5762
};
5863

@@ -97,7 +102,7 @@ class TabControllerScreen extends Component {
97102
}
98103

99104
render() {
100-
const {key, selectedIndex, asCarousel, items} = this.state;
105+
const {key, selectedIndex, asCarousel, centerSelected, items} = this.state;
101106
return (
102107
<View flex bg-grey70>
103108
<TabController
@@ -117,18 +122,29 @@ class TabControllerScreen extends Component {
117122
// iconColor={'green'}
118123
// selectedIconColor={'blue'}
119124
activeBackgroundColor={Colors.blue60}
125+
centerSelected={centerSelected}
120126
>
121127
{/* {this.renderTabItems()} */}
122128
</TabController.TabBar>
123129
{this.renderTabPages()}
124130
</TabController>
125-
<Button
126-
bg-grey20={!asCarousel}
127-
bg-green30={asCarousel}
128-
label={`Carousel:${asCarousel ? 'ON' : 'OFF'}`}
129-
style={{position: 'absolute', bottom: 100, right: 20}}
130-
onPress={this.toggleCarouselMode}
131-
/>
131+
<View absB left margin-20 marginB-100>
132+
<Button
133+
bg-grey20={!asCarousel}
134+
bg-green30={asCarousel}
135+
label={`Carousel : ${asCarousel ? 'ON' : 'OFF'}`}
136+
marginB-12
137+
size="small"
138+
onPress={this.toggleCarouselMode}
139+
/>
140+
<Button
141+
bg-grey20={!centerSelected}
142+
bg-green30={centerSelected}
143+
label={`centerSelected : ${centerSelected ? 'ON' : 'OFF'}`}
144+
size="small"
145+
onPress={this.toggleCenterSelected}
146+
/>
147+
</View>
132148
</View>
133149
);
134150
}

demo/src/screens/componentScreens/TabControllerScreen/tab1.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ class Tab1 extends Component {
66
state = {};
77
render() {
88
return (
9-
<View flex padding-20 spread center>
9+
<View flex padding-20>
1010
<Image
1111
style={StyleSheet.absoluteFillObject}
12+
overlayType="top"
1213
source={{
1314
uri:
1415
'https://images.unsplash.com/photo-1553969923-bbf0cac2666b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=668&q=80'
1516
}}
1617
/>
1718
<Text text40 white>
18-
TAB 1
19+
Home
1920
</Text>
20-
<Button marginT-20 label="Show Me"/>
21+
<View absR marginR-20>
22+
<Button marginT-20 round style={{width: 50}} size="small" iconSource={Assets.icons.search} white/>
23+
</View>
2124
</View>
2225
);
2326
}

demo/src/screens/componentScreens/TabControllerScreen/tab2.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ class Tab2 extends Component {
1616
render() {
1717
const {loading} = this.state;
1818
return (
19-
<View flex padding-20 center>
19+
<View flex padding-20>
2020
<Image
2121
style={StyleSheet.absoluteFillObject}
22+
overlayType="top"
2223
source={{
2324
uri:
2425
'https://images.unsplash.com/photo-1551376347-075b0121a65b?ixlib=rb-1.2.1&auto=format&fit=crop&w=2468&q=80'
2526
}}
2627
/>
27-
<Text text40>{loading ? 'Loading...' : ' TAB 2'}</Text>
28+
<Text text40 white>{loading ? 'Loading...' : ' Posts'}</Text>
2829
</View>
2930
);
3031
}

demo/src/screens/componentScreens/TabControllerScreen/tab3.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class Tab2 extends Component {
3636
return (
3737
<ScrollView>
3838
<View flex padding-20>
39-
<Text text40>TAB 3</Text>
39+
<Text text40>Reviews</Text>
4040

4141
{loading && (
4242
<Text marginT-20 text60>
@@ -45,7 +45,7 @@ class Tab2 extends Component {
4545
)}
4646

4747
{!loading &&
48-
_.times(100, index => {
48+
_.times(20, index => {
4949
return (
5050
<Card row centerV margin-20 padding-20 key={index} onPress={_.noop}>
5151
<Avatar

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"prop-types": "^15.5.10",
5050
"react-native-animatable": "^1.1.0",
5151
"react-native-color": "0.0.10",
52+
"react-native-redash": "^11.2.1",
5253
"react-native-text-size": "4.0.0-rc.1",
5354
"semver": "^5.5.0",
5455
"url-parse": "^1.2.0"

src/components/tabController/PageCarousel.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ class PageCarousel extends PureComponent {
2929
scrollToPage = (pageIndex, animated) => {
3030
const node = _.invoke(this.carousel, 'current.getNode');
3131
if (node) {
32-
node.scrollTo({x: pageIndex * Constants.screenWidth, animated});
32+
node.scrollTo({x: pageIndex * Constants.screenWidth, animated: false});
3333
}
3434
};
3535

3636
renderCodeBlock = () => {
37-
const {currentPage} = this.context;
38-
return block([Animated.onChange(currentPage, call([currentPage], this.onTabChange))]);
37+
const {targetPage} = this.context;
38+
return block([Animated.onChange(targetPage, [call([targetPage], this.onTabChange)])]);
3939
};
4040

4141
render() {

src/components/tabController/TabBar.js

Lines changed: 43 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// TODO: disable scroll when content width is shorter than screen width
33
import React, {PureComponent} from 'react';
44
import {StyleSheet, ScrollView, ViewPropTypes, Platform, Text as RNText} from 'react-native';
5-
import Reanimated, {Easing} from 'react-native-reanimated';
5+
import Reanimated from 'react-native-reanimated';
66
import PropTypes from 'prop-types';
77
import _ from 'lodash';
88

@@ -17,22 +17,7 @@ import {LogService} from '../../services';
1717

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

3722
/**
3823
* @description: TabController's TabBar component
@@ -95,7 +80,11 @@ class TabBar extends PureComponent {
9580
/**
9681
* The TabBar container width
9782
*/
98-
containerWidth: PropTypes.number
83+
containerWidth: PropTypes.number,
84+
/**
85+
* Pass to center selected item
86+
*/
87+
centerSelected: PropTypes.bool
9988
};
10089

10190
static defaultProps = {
@@ -115,6 +104,7 @@ class TabBar extends PureComponent {
115104
this.tabBarScrollOffset = 0;
116105

117106
this._itemsWidths = _.times(itemsCount, () => null);
107+
this._itemsOffsets = _.times(itemsCount, () => null);
118108
this._indicatorOffset = new Value(0);
119109
this._indicatorWidth = new Value(0);
120110

@@ -136,7 +126,7 @@ class TabBar extends PureComponent {
136126
}
137127

138128
get children() {
139-
return _.filter(this.props.children, child => !!child);
129+
return _.filter(this.props.children, (child) => !!child);
140130
}
141131

142132
get itemsCount() {
@@ -148,6 +138,11 @@ class TabBar extends PureComponent {
148138
}
149139
}
150140

141+
get centerOffset() {
142+
const {centerSelected} = this.props;
143+
return centerSelected ? Constants.screenWidth / 2 : 0;
144+
}
145+
151146
registerTabItems() {
152147
const {registerTabItems} = this.context;
153148
const {items} = this.props;
@@ -176,41 +171,44 @@ class TabBar extends PureComponent {
176171

177172
// TODO: move this logic into a ScrollPresenter or something
178173
focusSelected = ([index]) => {
179-
const {itemsOffsets, itemsWidths} = this.state;
180-
const itemOffset = itemsOffsets[index];
181-
const itemWidth = itemsWidths[index];
174+
const {centerSelected} = this.props;
175+
const itemOffset = this._itemsOffsets[index];
176+
const itemWidth = this._itemsWidths[index];
177+
182178
if (itemOffset && itemWidth) {
183-
if (itemOffset < this.tabBarScrollOffset) {
179+
if (centerSelected) {
180+
this.tabBar.current.scrollTo({x: itemOffset - this.centerOffset + itemWidth / 2});
181+
} else if (itemOffset < this.tabBarScrollOffset) {
184182
this.tabBar.current.scrollTo({x: itemOffset - itemWidth});
185183
} else if (itemOffset + itemWidth > this.tabBarScrollOffset + this.containerWidth) {
186184
const offsetChange = Math.max(0, itemOffset - (this.tabBarScrollOffset + this.containerWidth));
187185
this.tabBar.current.scrollTo({x: this.tabBarScrollOffset + offsetChange + itemWidth});
188186
}
189187
}
190-
}
188+
};
191189

192-
onItemLayout = (itemWidth, itemIndex) => {
193-
this._itemsWidths[itemIndex] = itemWidth;
190+
onItemLayout = ({width, x}, itemIndex) => {
191+
this._itemsWidths[itemIndex] = width;
192+
this._itemsOffsets[itemIndex] = x;
194193
if (!_.includes(this._itemsWidths, null)) {
195194
const {selectedIndex} = this.context;
196-
const itemsOffsets = _.map(this._itemsWidths,
197-
(w, index) => INDICATOR_INSET + _.sum(_.take(this._itemsWidths, index)));
198-
const itemsWidths = _.map(this._itemsWidths, width => width - INDICATOR_INSET * 2);
195+
const itemsOffsets = _.map(this._itemsOffsets, offset => offset + INDICATOR_INSET);
196+
const itemsWidths = _.map(this._itemsWidths, (width) => width - INDICATOR_INSET * 2);
199197

200198
this.setState({itemsWidths, itemsOffsets});
201199
const selectedItemOffset = itemsOffsets[selectedIndex] - INDICATOR_INSET;
202-
203-
if (selectedItemOffset + this._itemsWidths[selectedIndex] > Constants.screenWidth) {
200+
201+
if (selectedItemOffset + this._itemsWidths[selectedIndex] > Constants.screenWidth) {
204202
this.tabBar.current.scrollTo({x: selectedItemOffset, animated: true});
205203
}
206204
}
207205
};
208206

209207
onScroll = ({nativeEvent: {contentOffset}}) => {
210208
this.tabBarScrollOffset = contentOffset.x;
211-
}
209+
};
212210

213-
onContentSizeChange = width => {
211+
onContentSizeChange = (width) => {
214212
if (width > this.containerWidth) {
215213
this.setState({scrollEnabled: true});
216214
}
@@ -288,29 +286,19 @@ class TabBar extends PureComponent {
288286
}
289287

290288
renderCodeBlock = () => {
291-
const {carouselOffset, asCarousel, currentPage} = this.context;
289+
const {currentPage, targetPage} = this.context;
292290
const {itemsWidths, itemsOffsets} = this.state;
293291
const nodes = [];
294292

295-
if (asCarousel) {
296-
nodes.push(set(this._indicatorOffset,
297-
interpolate(carouselOffset, {
298-
inputRange: itemsOffsets.map((value, index) => index * Constants.screenWidth),
299-
outputRange: itemsOffsets,
300-
extrapolate: Extrapolate.CLAMP
301-
})),
302-
set(this._indicatorWidth,
303-
interpolate(carouselOffset, {
304-
inputRange: itemsWidths.map((value, index) => index * Constants.screenWidth),
305-
outputRange: itemsWidths,
306-
extrapolate: Extrapolate.CLAMP
307-
})));
308-
} else {
309-
nodes.push(set(this._indicatorOffset, runIndicatorTimer(new Clock(), currentPage, itemsOffsets)),
310-
set(this._indicatorWidth, runIndicatorTimer(new Clock(), currentPage, itemsWidths)));
311-
}
293+
nodes.push(set(this._indicatorOffset,
294+
interpolate(currentPage, {
295+
inputRange: itemsOffsets.map((v, i) => i),
296+
outputRange: itemsOffsets
297+
})));
298+
nodes.push(set(this._indicatorWidth,
299+
interpolate(currentPage, {inputRange: itemsWidths.map((v, i) => i), outputRange: itemsWidths})));
312300

313-
nodes.push(Reanimated.onChange(currentPage, Reanimated.call([currentPage], this.focusSelected)));
301+
nodes.push(Reanimated.onChange(targetPage, Reanimated.call([targetPage], this.focusSelected)));
314302

315303
return block(nodes);
316304
};
@@ -334,7 +322,9 @@ class TabBar extends PureComponent {
334322
scrollEventThrottle={100}
335323
testID={testID}
336324
>
337-
<View style={[styles.tabBar, height && {height}]}>{this.renderTabBarItems()}</View>
325+
<View style={[styles.tabBar, height && {height}, {paddingHorizontal: this.centerOffset}]}>
326+
{this.renderTabBarItems()}
327+
</View>
338328
{this.renderSelectedIndicator()}
339329
</ScrollView>
340330
{_.size(itemsWidths) > 1 && <Code>{this.renderCodeBlock}</Code>}
@@ -389,38 +379,4 @@ const styles = StyleSheet.create({
389379
}
390380
});
391381

392-
function runIndicatorTimer(clock, currentPage, values) {
393-
const state = {
394-
finished: new Value(0),
395-
position: new Value(0),
396-
time: new Value(0),
397-
frameTime: new Value(0)
398-
};
399-
400-
const config = {
401-
duration: 200,
402-
toValue: new Value(100),
403-
easing: Easing.inOut(Easing.ease)
404-
};
405-
406-
return block([
407-
..._.map(values, (value, index) => {
408-
return cond(and(eq(currentPage, index), neq(config.toValue, index)), [
409-
set(state.finished, 0),
410-
set(state.time, 0),
411-
set(state.frameTime, 0),
412-
set(config.toValue, index),
413-
startClock(clock)
414-
]);
415-
}),
416-
timing(clock, state, config),
417-
cond(state.finished, stopClock(clock)),
418-
interpolate(state.position, {
419-
inputRange: _.times(values.length),
420-
outputRange: values,
421-
extrapolate: Extrapolate.CLAMP
422-
})
423-
]);
424-
}
425-
426382
export default asBaseComponent(forwardRef(TabBar));

0 commit comments

Comments
 (0)