Skip to content

Improve tabbar animation & performance #495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions demo/src/screens/incubatorScreens/TabControllerScreen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class TabControllerScreen extends Component {
}
};

getTabs() {
renderTabItems() {
const {tabsCount} = this.state;
const tabs = [
<Incubator.TabController.TabBarItem key="tab1" label="tab1" onPress={() => console.warn('press tab1')} />,
Expand All @@ -52,6 +52,31 @@ class TabControllerScreen extends Component {
];
}

renderTabPages() {
return (
<View flex>
<Incubator.TabController.TabPage index={0}>
<Tab1 />
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={1} lazy>
<Tab2 />
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={2}>
<Tab3 />
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={3}>
<Text text40>ACCOUNT</Text>
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={4}>
<Text text40>GROUPS</Text>
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={5}>
<Text text40>BLOG</Text>
</Incubator.TabController.TabPage>
</View>
);
}

render() {
const {key, selectedIndex} = this.state;
return (
Expand All @@ -73,28 +98,9 @@ class TabControllerScreen extends Component {
// selectedIconColor={'blue'}
activeBackgroundColor={Colors.blue60}
>
{this.getTabs()}
{this.renderTabItems()}
</Incubator.TabController.TabBar>
<View flex>
<Incubator.TabController.TabPage index={0}>
<Tab1 />
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={1} lazy>
<Tab2 />
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={2}>
<Tab3 />
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={3}>
<Text text40>ACCOUNT</Text>
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={4}>
<Text text40>GROUPS</Text>
</Incubator.TabController.TabPage>
<Incubator.TabController.TabPage index={5}>
<Text text40>BLOG</Text>
</Incubator.TabController.TabPage>
</View>
{this.renderTabPages()}
</Incubator.TabController>
</View>
</View>
Expand Down
30 changes: 26 additions & 4 deletions demo/src/screens/incubatorScreens/TabControllerScreen/tab3.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {Component} from 'react';
import {ScrollView} from 'react-native';
import _ from 'lodash';
import {Card, Incubator, Colors, View, Text, Image, Assets, Button} from 'react-native-ui-lib'; //eslint-disable-line
import {Card, Avatar, View, Text, Image, Assets, Button} from 'react-native-ui-lib'; //eslint-disable-line

class Tab2 extends Component {
state = {
Expand All @@ -12,6 +12,22 @@ class Tab2 extends Component {
setTimeout(() => {
this.setState({loading: false});
}, 1200);

// this.slow();
}

slow(iterations = 10) {
if (iterations === 0) {
return;
}

setTimeout(() => {
_.times(5000, () => {
console.log('slow log');
});

this.slow(iterations - 1);
}, 10);
}

render() {
Expand All @@ -29,10 +45,16 @@ class Tab2 extends Component {
)}

{!loading &&
_.times(20, index => {
_.times(100, index => {
return (
<Card margin-20 padding-20 key={index} onPress={_.noop}>
<Text text40>{index}</Text>
<Card row centerV margin-20 padding-20 key={index} onPress={_.noop}>
<Avatar
size={50}
imageSource={{
uri: 'https://static.pexels.com/photos/60628/flower-garden-blue-sky-hokkaido-japan-60628.jpeg',
}}
/>
<Text text40 marginL-20>{index}</Text>
</Card>
);
})}
Expand Down
48 changes: 48 additions & 0 deletions src/incubator/TabController/ReanimatedObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Reanimated, {Easing} from 'react-native-reanimated';

const {Clock, Value, cond, stopClock, startClock, clockRunning, timing, block, set} = Reanimated;

export default class ReanimatedObject {
constructor(config) {
this.clock = new Clock();
this.state = {
finished: new Value(0),
position: new Value(0),
time: new Value(0),
frameTime: new Value(0),
};
(this.config = {
duration: 300,
toValue: new Value(0),
easing: Easing.bezier(0.23, 1, 0.32, 1),
...config,
}),
(this.prevValue = new Value(0));
this.value = new Value(0);
this.nextValue = new Value(0);
}

getTransitionBlock() {
return block([
cond(
clockRunning(this.clock),
[
set(this.config.toValue, this.nextValue),
set(this.value, this.state.position),
],
[
set(this.state.finished, 0),
set(this.state.time, 0),
set(this.state.frameTime, 0),
set(this.config.toValue, this.nextValue),
startClock(this.clock),
],
),
timing(this.clock, this.state, this.config),
cond(this.state.finished, [
stopClock(this.clock),
set(this.prevValue, this.state.position),
]),
]);
}
}
105 changes: 61 additions & 44 deletions src/incubator/TabController/TabBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,22 @@ import {StyleSheet, ScrollView, ViewPropTypes, Platform} from 'react-native';
import Reanimated, {Easing} from 'react-native-reanimated';
import PropTypes from 'prop-types';
import _ from 'lodash';

import TabBarContext from './TabBarContext';
import ReanimatedObject from './ReanimatedObject';
import {asBaseComponent, forwardRef} from '../../commons';
import View from '../../components/view';
import Text from '../../components/text';
import {Colors, Spacings} from '../../style';
import {Constants} from '../../helpers';

const DEFAULT_HEIGHT = 48;
const {
Code,
Clock,
Value,
add,
sub,
cond,
eq,
stopClock,
startClock,
clockRunning,
timing,
block,
set,
} = Reanimated;
const {Code, Clock, Value, add, sub, cond, eq, stopClock, startClock, clockRunning, timing, block, set} = Reanimated;

/**
* @description: TabController's TabBar component
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/incubatorScreens/TabControllerScreen/index.js
*/
* @description: TabController's TabBar component
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/incubatorScreens/TabControllerScreen/index.js
*/
class TabBar extends PureComponent {
static displayName = 'TabController.TabBar';
static contextType = TabBarContext;
Expand Down Expand Up @@ -93,23 +81,26 @@ class TabBar extends PureComponent {
containerWidth: Constants.screenWidth,
};

state = {
itemsWidths: undefined,
};

tabBar = React.createRef();
_clock = new Clock();
_itemsWidths = _.times(React.Children.count(this.props.children), () => null);
_indicatorWidth = new Value(0);
_prevIndicatorWidth = new Value(0);
_prevOffset = new Value(0);
_offset = new Value(0);

constructor(props, context) {
super(props, context);
const {registerTabItems} = this.context;
const itemsCount = React.Children.count(this.props.children);
const ignoredItems = [];

this.tabBar = React.createRef();

this._itemsWidths = _.times(React.Children.count(this.props.children), () => null);
this._indicatorOffset = new ReanimatedObject({duration: 300, easing: Easing.bezier(0.23, 1, 0.32, 1)});
this._indicatorWidth = new ReanimatedObject({duration: 300, easing: Easing.bezier(0.23, 1, 0.32, 1)});
this._indicatorTransitionStyle = {
width: this._indicatorWidth.value,
left: this._indicatorOffset.value,
};

this.state = {
itemsWidths: undefined,
};

React.Children.toArray(this.props.children).forEach((child, index) => {
if (child.props.ignore) {
ignoredItems.push(index);
Expand Down Expand Up @@ -158,14 +149,19 @@ class TabBar extends PureComponent {
}

renderSelectedIndicator() {
// return null;
const {itemsWidths} = this.state;
const {indicatorStyle} = this.props;
if (itemsWidths) {
const transitionStyle = {
width: this.runTiming(this._indicatorWidth, this._prevIndicatorWidth, 300),
left: this.runTiming(this._offset, this._prevOffset, 400),
};
return <Reanimated.View style={[styles.selectedIndicator, indicatorStyle, transitionStyle]} />;
// const transitionStyle = {
// width: this.runTiming(this._indicatorWidth, this._prevIndicatorWidth, 300),
// left: this.runTiming(this._offset, this._prevOffset, 400),
// };
return (
<Reanimated.View
style={[styles.selectedIndicator, indicatorStyle /* , transitionStyle */, this._indicatorTransitionStyle]}
/>
);
}
}

Expand All @@ -181,7 +177,12 @@ class TabBar extends PureComponent {
activeBackgroundColor,
} = this.props;
if (!_.isEmpty(itemStates)) {
return React.Children.map(this.props.children, (child, index) => {

if (this.tabBarItems) {
return this.tabBarItems;
}

this.tabBarItems = React.Children.map(this.props.children, (child, index) => {
return React.cloneElement(child, {
labelColor,
selectedLabelColor,
Expand All @@ -197,6 +198,7 @@ class TabBar extends PureComponent {
onLayout: this.onItemLayout,
});
});
return this.tabBarItems;
}
}

Expand All @@ -210,8 +212,8 @@ class TabBar extends PureComponent {
ref={this.tabBar}
horizontal
showsHorizontalScrollIndicator={false}
style={{backgroundColor: Colors.white}}
contentContainerStyle={{minWidth: containerWidth}}
style={styles.tabBarScroll}
contentContainerStyle={styles.tabBarScrollContent}
>
<View style={[styles.tabBar, height && {height}]}>{this.renderTabBarItems()}</View>
{this.renderSelectedIndicator()}
Expand All @@ -222,14 +224,23 @@ class TabBar extends PureComponent {
const indicatorInset = Spacings.s4;

return block([
// calc indicator current width
..._.map(itemsWidths, (width, index) => {
return cond(eq(currentPage, index), [
set(this._indicatorWidth, sub(itemsWidths[index], indicatorInset * 2)),
set(this._indicatorWidth.nextValue, sub(itemsWidths[index], indicatorInset * 2)),
]);
}),
// calc indicator current position
..._.map(itemsOffsets, (offset, index) => {
return cond(eq(currentPage, index), [set(this._offset, add(itemsOffsets[index], indicatorInset))]);
return cond(eq(currentPage, index), [
set(this._indicatorOffset.nextValue, add(itemsOffsets[index], indicatorInset)),
]);
}),

// Offset transition
this._indicatorOffset.getTransitionBlock(),
// Width transition
this._indicatorWidth.getTransitionBlock(),
]);
}}
</Code>
Expand All @@ -247,6 +258,12 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
},
tabBarScroll: {
backgroundColor: Colors.white,
},
tabBarScrollContent: {
minWidth: Constants.screenWidth,
},
tab: {
flex: 1,
alignItems: 'center',
Expand All @@ -266,13 +283,13 @@ const styles = StyleSheet.create({
shadowColor: Colors.dark10,
shadowOpacity: 0.05,
shadowRadius: 2,
shadowOffset: {height: 6, width: 0}
shadowOffset: {height: 6, width: 0},
},
android: {
elevation: 5,
backgroundColor: Colors.white
}
})
backgroundColor: Colors.white,
},
}),
},
});

Expand Down