Skip to content

Commit

Permalink
Refactor/tab controller (#739)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ethanshar committed Apr 12, 2020
1 parent 6b2dfaf commit bfd2a70
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 145 deletions.
42 changes: 29 additions & 13 deletions demo/src/screens/componentScreens/TabControllerScreen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ const TABS = ['Home', 'Posts', 'Reviews', 'Videos', 'Photos', 'Events', 'About',
class TabControllerScreen extends Component {
state = {
asCarousel: true,
centerSelected: false,
selectedIndex: 0,
items: _.chain(TABS)
.map(tab => ({label: tab, key: tab}))
.map((tab) => ({label: tab, key: tab}))
.value(),
key: Date.now()
};
Expand All @@ -42,17 +43,21 @@ class TabControllerScreen extends Component {
}
};




toggleCarouselMode = () => {
this.setState({
asCarousel: !this.state.asCarousel,
key: this.state.asCarousel ? 'asCarousel' : 'staticPages'
});
};

onChangeIndex = selectedIndex => {
toggleCenterSelected = () => {
this.setState({
centerSelected: !this.state.centerSelected,
key: Date.now()
});
};

onChangeIndex = (selectedIndex) => {
this.setState({selectedIndex});
};

Expand Down Expand Up @@ -97,7 +102,7 @@ class TabControllerScreen extends Component {
}

render() {
const {key, selectedIndex, asCarousel, items} = this.state;
const {key, selectedIndex, asCarousel, centerSelected, items} = this.state;
return (
<View flex bg-grey70>
<TabController
Expand All @@ -117,18 +122,29 @@ class TabControllerScreen extends Component {
// iconColor={'green'}
// selectedIconColor={'blue'}
activeBackgroundColor={Colors.blue60}
centerSelected={centerSelected}
>
{/* {this.renderTabItems()} */}
</TabController.TabBar>
{this.renderTabPages()}
</TabController>
<Button
bg-grey20={!asCarousel}
bg-green30={asCarousel}
label={`Carousel:${asCarousel ? 'ON' : 'OFF'}`}
style={{position: 'absolute', bottom: 100, right: 20}}
onPress={this.toggleCarouselMode}
/>
<View absB left margin-20 marginB-100>
<Button
bg-grey20={!asCarousel}
bg-green30={asCarousel}
label={`Carousel : ${asCarousel ? 'ON' : 'OFF'}`}
marginB-12
size="small"
onPress={this.toggleCarouselMode}
/>
<Button
bg-grey20={!centerSelected}
bg-green30={centerSelected}
label={`centerSelected : ${centerSelected ? 'ON' : 'OFF'}`}
size="small"
onPress={this.toggleCenterSelected}
/>
</View>
</View>
);
}
Expand Down
9 changes: 6 additions & 3 deletions demo/src/screens/componentScreens/TabControllerScreen/tab1.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ class Tab1 extends Component {
state = {};
render() {
return (
<View flex padding-20 spread center>
<View flex padding-20>
<Image
style={StyleSheet.absoluteFillObject}
overlayType="top"
source={{
uri:
'https://images.unsplash.com/photo-1553969923-bbf0cac2666b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=668&q=80'
}}
/>
<Text text40 white>
TAB 1
Home
</Text>
<Button marginT-20 label="Show Me"/>
<View absR marginR-20>
<Button marginT-20 round style={{width: 50}} size="small" iconSource={Assets.icons.search} white/>
</View>
</View>
);
}
Expand Down
5 changes: 3 additions & 2 deletions demo/src/screens/componentScreens/TabControllerScreen/tab2.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ class Tab2 extends Component {
render() {
const {loading} = this.state;
return (
<View flex padding-20 center>
<View flex padding-20>
<Image
style={StyleSheet.absoluteFillObject}
overlayType="top"
source={{
uri:
'https://images.unsplash.com/photo-1551376347-075b0121a65b?ixlib=rb-1.2.1&auto=format&fit=crop&w=2468&q=80'
}}
/>
<Text text40>{loading ? 'Loading...' : ' TAB 2'}</Text>
<Text text40 white>{loading ? 'Loading...' : ' Posts'}</Text>
</View>
);
}
Expand Down
4 changes: 2 additions & 2 deletions demo/src/screens/componentScreens/TabControllerScreen/tab3.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Tab2 extends Component {
return (
<ScrollView>
<View flex padding-20>
<Text text40>TAB 3</Text>
<Text text40>Reviews</Text>

{loading && (
<Text marginT-20 text60>
Expand All @@ -45,7 +45,7 @@ class Tab2 extends Component {
)}

{!loading &&
_.times(100, index => {
_.times(20, index => {
return (
<Card row centerV margin-20 padding-20 key={index} onPress={_.noop}>
<Avatar
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"prop-types": "^15.5.10",
"react-native-animatable": "^1.1.0",
"react-native-color": "0.0.10",
"react-native-redash": "^11.2.1",
"react-native-text-size": "4.0.0-rc.1",
"semver": "^5.5.0",
"url-parse": "^1.2.0"
Expand Down
6 changes: 3 additions & 3 deletions src/components/tabController/PageCarousel.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ class PageCarousel extends PureComponent {
scrollToPage = (pageIndex, animated) => {
const node = _.invoke(this.carousel, 'current.getNode');
if (node) {
node.scrollTo({x: pageIndex * Constants.screenWidth, animated});
node.scrollTo({x: pageIndex * Constants.screenWidth, animated: false});
}
};

renderCodeBlock = () => {
const {currentPage} = this.context;
return block([Animated.onChange(currentPage, call([currentPage], this.onTabChange))]);
const {targetPage} = this.context;
return block([Animated.onChange(targetPage, [call([targetPage], this.onTabChange)])]);
};

render() {
Expand Down
130 changes: 43 additions & 87 deletions src/components/tabController/TabBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// TODO: disable scroll when content width is shorter than screen width
import React, {PureComponent} from 'react';
import {StyleSheet, ScrollView, ViewPropTypes, Platform, Text as RNText} from 'react-native';
import Reanimated, {Easing} from 'react-native-reanimated';
import Reanimated from 'react-native-reanimated';
import PropTypes from 'prop-types';
import _ from 'lodash';

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

const DEFAULT_HEIGHT = 48;
const INDICATOR_INSET = Spacings.s4;
const {
Code,
Clock,
Value,
and,
eq,
neq,
cond,
stopClock,
startClock,
interpolate,
Extrapolate,
timing,
block,
set
} = Reanimated;
const {Code, Value, interpolate, block, set} = Reanimated;

/**
* @description: TabController's TabBar component
Expand Down Expand Up @@ -95,7 +80,11 @@ class TabBar extends PureComponent {
/**
* The TabBar container width
*/
containerWidth: PropTypes.number
containerWidth: PropTypes.number,
/**
* Pass to center selected item
*/
centerSelected: PropTypes.bool
};

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

this._itemsWidths = _.times(itemsCount, () => null);
this._itemsOffsets = _.times(itemsCount, () => null);
this._indicatorOffset = new Value(0);
this._indicatorWidth = new Value(0);

Expand All @@ -136,7 +126,7 @@ class TabBar extends PureComponent {
}

get children() {
return _.filter(this.props.children, child => !!child);
return _.filter(this.props.children, (child) => !!child);
}

get itemsCount() {
Expand All @@ -148,6 +138,11 @@ class TabBar extends PureComponent {
}
}

get centerOffset() {
const {centerSelected} = this.props;
return centerSelected ? Constants.screenWidth / 2 : 0;
}

registerTabItems() {
const {registerTabItems} = this.context;
const {items} = this.props;
Expand Down Expand Up @@ -176,41 +171,44 @@ class TabBar extends PureComponent {

// TODO: move this logic into a ScrollPresenter or something
focusSelected = ([index]) => {
const {itemsOffsets, itemsWidths} = this.state;
const itemOffset = itemsOffsets[index];
const itemWidth = itemsWidths[index];
const {centerSelected} = this.props;
const itemOffset = this._itemsOffsets[index];
const itemWidth = this._itemsWidths[index];

if (itemOffset && itemWidth) {
if (itemOffset < this.tabBarScrollOffset) {
if (centerSelected) {
this.tabBar.current.scrollTo({x: itemOffset - this.centerOffset + itemWidth / 2});
} else if (itemOffset < this.tabBarScrollOffset) {
this.tabBar.current.scrollTo({x: itemOffset - itemWidth});
} else if (itemOffset + itemWidth > this.tabBarScrollOffset + this.containerWidth) {
const offsetChange = Math.max(0, itemOffset - (this.tabBarScrollOffset + this.containerWidth));
this.tabBar.current.scrollTo({x: this.tabBarScrollOffset + offsetChange + itemWidth});
}
}
}
};

onItemLayout = (itemWidth, itemIndex) => {
this._itemsWidths[itemIndex] = itemWidth;
onItemLayout = ({width, x}, itemIndex) => {
this._itemsWidths[itemIndex] = width;
this._itemsOffsets[itemIndex] = x;
if (!_.includes(this._itemsWidths, null)) {
const {selectedIndex} = this.context;
const itemsOffsets = _.map(this._itemsWidths,
(w, index) => INDICATOR_INSET + _.sum(_.take(this._itemsWidths, index)));
const itemsWidths = _.map(this._itemsWidths, width => width - INDICATOR_INSET * 2);
const itemsOffsets = _.map(this._itemsOffsets, offset => offset + INDICATOR_INSET);
const itemsWidths = _.map(this._itemsWidths, (width) => width - INDICATOR_INSET * 2);

this.setState({itemsWidths, itemsOffsets});
const selectedItemOffset = itemsOffsets[selectedIndex] - INDICATOR_INSET;
if (selectedItemOffset + this._itemsWidths[selectedIndex] > Constants.screenWidth) {

if (selectedItemOffset + this._itemsWidths[selectedIndex] > Constants.screenWidth) {
this.tabBar.current.scrollTo({x: selectedItemOffset, animated: true});
}
}
};

onScroll = ({nativeEvent: {contentOffset}}) => {
this.tabBarScrollOffset = contentOffset.x;
}
};

onContentSizeChange = width => {
onContentSizeChange = (width) => {
if (width > this.containerWidth) {
this.setState({scrollEnabled: true});
}
Expand Down Expand Up @@ -288,29 +286,19 @@ class TabBar extends PureComponent {
}

renderCodeBlock = () => {
const {carouselOffset, asCarousel, currentPage} = this.context;
const {currentPage, targetPage} = this.context;
const {itemsWidths, itemsOffsets} = this.state;
const nodes = [];

if (asCarousel) {
nodes.push(set(this._indicatorOffset,
interpolate(carouselOffset, {
inputRange: itemsOffsets.map((value, index) => index * Constants.screenWidth),
outputRange: itemsOffsets,
extrapolate: Extrapolate.CLAMP
})),
set(this._indicatorWidth,
interpolate(carouselOffset, {
inputRange: itemsWidths.map((value, index) => index * Constants.screenWidth),
outputRange: itemsWidths,
extrapolate: Extrapolate.CLAMP
})));
} else {
nodes.push(set(this._indicatorOffset, runIndicatorTimer(new Clock(), currentPage, itemsOffsets)),
set(this._indicatorWidth, runIndicatorTimer(new Clock(), currentPage, itemsWidths)));
}
nodes.push(set(this._indicatorOffset,
interpolate(currentPage, {
inputRange: itemsOffsets.map((v, i) => i),
outputRange: itemsOffsets
})));
nodes.push(set(this._indicatorWidth,
interpolate(currentPage, {inputRange: itemsWidths.map((v, i) => i), outputRange: itemsWidths})));

nodes.push(Reanimated.onChange(currentPage, Reanimated.call([currentPage], this.focusSelected)));
nodes.push(Reanimated.onChange(targetPage, Reanimated.call([targetPage], this.focusSelected)));

return block(nodes);
};
Expand All @@ -334,7 +322,9 @@ class TabBar extends PureComponent {
scrollEventThrottle={100}
testID={testID}
>
<View style={[styles.tabBar, height && {height}]}>{this.renderTabBarItems()}</View>
<View style={[styles.tabBar, height && {height}, {paddingHorizontal: this.centerOffset}]}>
{this.renderTabBarItems()}
</View>
{this.renderSelectedIndicator()}
</ScrollView>
{_.size(itemsWidths) > 1 && <Code>{this.renderCodeBlock}</Code>}
Expand Down Expand Up @@ -389,38 +379,4 @@ const styles = StyleSheet.create({
}
});

function runIndicatorTimer(clock, currentPage, values) {
const state = {
finished: new Value(0),
position: new Value(0),
time: new Value(0),
frameTime: new Value(0)
};

const config = {
duration: 200,
toValue: new Value(100),
easing: Easing.inOut(Easing.ease)
};

return block([
..._.map(values, (value, index) => {
return cond(and(eq(currentPage, index), neq(config.toValue, index)), [
set(state.finished, 0),
set(state.time, 0),
set(state.frameTime, 0),
set(config.toValue, index),
startClock(clock)
]);
}),
timing(clock, state, config),
cond(state.finished, stopClock(clock)),
interpolate(state.position, {
inputRange: _.times(values.length),
outputRange: values,
extrapolate: Extrapolate.CLAMP
})
]);
}

export default asBaseComponent(forwardRef(TabBar));

0 comments on commit bfd2a70

Please sign in to comment.