Skip to content

Commit 98fb02c

Browse files
authored
Improve tabbar animation & performance (#495)
* WIP - imporove tabbar animation * refactor code, make some performance improvments
1 parent a76f59a commit 98fb02c

File tree

4 files changed

+163
-70
lines changed

4 files changed

+163
-70
lines changed

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

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class TabControllerScreen extends Component {
3535
}
3636
};
3737

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

55+
renderTabPages() {
56+
return (
57+
<View flex>
58+
<Incubator.TabController.TabPage index={0}>
59+
<Tab1 />
60+
</Incubator.TabController.TabPage>
61+
<Incubator.TabController.TabPage index={1} lazy>
62+
<Tab2 />
63+
</Incubator.TabController.TabPage>
64+
<Incubator.TabController.TabPage index={2}>
65+
<Tab3 />
66+
</Incubator.TabController.TabPage>
67+
<Incubator.TabController.TabPage index={3}>
68+
<Text text40>ACCOUNT</Text>
69+
</Incubator.TabController.TabPage>
70+
<Incubator.TabController.TabPage index={4}>
71+
<Text text40>GROUPS</Text>
72+
</Incubator.TabController.TabPage>
73+
<Incubator.TabController.TabPage index={5}>
74+
<Text text40>BLOG</Text>
75+
</Incubator.TabController.TabPage>
76+
</View>
77+
);
78+
}
79+
5580
render() {
5681
const {key, selectedIndex} = this.state;
5782
return (
@@ -73,28 +98,9 @@ class TabControllerScreen extends Component {
7398
// selectedIconColor={'blue'}
7499
activeBackgroundColor={Colors.blue60}
75100
>
76-
{this.getTabs()}
101+
{this.renderTabItems()}
77102
</Incubator.TabController.TabBar>
78-
<View flex>
79-
<Incubator.TabController.TabPage index={0}>
80-
<Tab1 />
81-
</Incubator.TabController.TabPage>
82-
<Incubator.TabController.TabPage index={1} lazy>
83-
<Tab2 />
84-
</Incubator.TabController.TabPage>
85-
<Incubator.TabController.TabPage index={2}>
86-
<Tab3 />
87-
</Incubator.TabController.TabPage>
88-
<Incubator.TabController.TabPage index={3}>
89-
<Text text40>ACCOUNT</Text>
90-
</Incubator.TabController.TabPage>
91-
<Incubator.TabController.TabPage index={4}>
92-
<Text text40>GROUPS</Text>
93-
</Incubator.TabController.TabPage>
94-
<Incubator.TabController.TabPage index={5}>
95-
<Text text40>BLOG</Text>
96-
</Incubator.TabController.TabPage>
97-
</View>
103+
{this.renderTabPages()}
98104
</Incubator.TabController>
99105
</View>
100106
</View>

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {Component} from 'react';
22
import {ScrollView} from 'react-native';
33
import _ from 'lodash';
4-
import {Card, Incubator, Colors, View, Text, Image, Assets, Button} from 'react-native-ui-lib'; //eslint-disable-line
4+
import {Card, Avatar, View, Text, Image, Assets, Button} from 'react-native-ui-lib'; //eslint-disable-line
55

66
class Tab2 extends Component {
77
state = {
@@ -12,6 +12,22 @@ class Tab2 extends Component {
1212
setTimeout(() => {
1313
this.setState({loading: false});
1414
}, 1200);
15+
16+
// this.slow();
17+
}
18+
19+
slow(iterations = 10) {
20+
if (iterations === 0) {
21+
return;
22+
}
23+
24+
setTimeout(() => {
25+
_.times(5000, () => {
26+
console.log('slow log');
27+
});
28+
29+
this.slow(iterations - 1);
30+
}, 10);
1531
}
1632

1733
render() {
@@ -29,10 +45,16 @@ class Tab2 extends Component {
2945
)}
3046

3147
{!loading &&
32-
_.times(20, index => {
48+
_.times(100, index => {
3349
return (
34-
<Card margin-20 padding-20 key={index} onPress={_.noop}>
35-
<Text text40>{index}</Text>
50+
<Card row centerV margin-20 padding-20 key={index} onPress={_.noop}>
51+
<Avatar
52+
size={50}
53+
imageSource={{
54+
uri: 'https://static.pexels.com/photos/60628/flower-garden-blue-sky-hokkaido-japan-60628.jpeg',
55+
}}
56+
/>
57+
<Text text40 marginL-20>{index}</Text>
3658
</Card>
3759
);
3860
})}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Reanimated, {Easing} from 'react-native-reanimated';
2+
3+
const {Clock, Value, cond, stopClock, startClock, clockRunning, timing, block, set} = Reanimated;
4+
5+
export default class ReanimatedObject {
6+
constructor(config) {
7+
this.clock = new Clock();
8+
this.state = {
9+
finished: new Value(0),
10+
position: new Value(0),
11+
time: new Value(0),
12+
frameTime: new Value(0),
13+
};
14+
(this.config = {
15+
duration: 300,
16+
toValue: new Value(0),
17+
easing: Easing.bezier(0.23, 1, 0.32, 1),
18+
...config,
19+
}),
20+
(this.prevValue = new Value(0));
21+
this.value = new Value(0);
22+
this.nextValue = new Value(0);
23+
}
24+
25+
getTransitionBlock() {
26+
return block([
27+
cond(
28+
clockRunning(this.clock),
29+
[
30+
set(this.config.toValue, this.nextValue),
31+
set(this.value, this.state.position),
32+
],
33+
[
34+
set(this.state.finished, 0),
35+
set(this.state.time, 0),
36+
set(this.state.frameTime, 0),
37+
set(this.config.toValue, this.nextValue),
38+
startClock(this.clock),
39+
],
40+
),
41+
timing(this.clock, this.state, this.config),
42+
cond(this.state.finished, [
43+
stopClock(this.clock),
44+
set(this.prevValue, this.state.position),
45+
]),
46+
]);
47+
}
48+
}

src/incubator/TabController/TabBar.js

Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,22 @@ import {StyleSheet, ScrollView, ViewPropTypes, Platform} from 'react-native';
55
import Reanimated, {Easing} from 'react-native-reanimated';
66
import PropTypes from 'prop-types';
77
import _ from 'lodash';
8+
89
import TabBarContext from './TabBarContext';
10+
import ReanimatedObject from './ReanimatedObject';
911
import {asBaseComponent, forwardRef} from '../../commons';
1012
import View from '../../components/view';
1113
import Text from '../../components/text';
1214
import {Colors, Spacings} from '../../style';
1315
import {Constants} from '../../helpers';
1416

1517
const DEFAULT_HEIGHT = 48;
16-
const {
17-
Code,
18-
Clock,
19-
Value,
20-
add,
21-
sub,
22-
cond,
23-
eq,
24-
stopClock,
25-
startClock,
26-
clockRunning,
27-
timing,
28-
block,
29-
set,
30-
} = Reanimated;
18+
const {Code, Clock, Value, add, sub, cond, eq, stopClock, startClock, clockRunning, timing, block, set} = Reanimated;
3119

3220
/**
33-
* @description: TabController's TabBar component
34-
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/incubatorScreens/TabControllerScreen/index.js
35-
*/
21+
* @description: TabController's TabBar component
22+
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/incubatorScreens/TabControllerScreen/index.js
23+
*/
3624
class TabBar extends PureComponent {
3725
static displayName = 'TabController.TabBar';
3826
static contextType = TabBarContext;
@@ -93,23 +81,26 @@ class TabBar extends PureComponent {
9381
containerWidth: Constants.screenWidth,
9482
};
9583

96-
state = {
97-
itemsWidths: undefined,
98-
};
99-
100-
tabBar = React.createRef();
101-
_clock = new Clock();
102-
_itemsWidths = _.times(React.Children.count(this.props.children), () => null);
103-
_indicatorWidth = new Value(0);
104-
_prevIndicatorWidth = new Value(0);
105-
_prevOffset = new Value(0);
106-
_offset = new Value(0);
107-
10884
constructor(props, context) {
10985
super(props, context);
11086
const {registerTabItems} = this.context;
11187
const itemsCount = React.Children.count(this.props.children);
11288
const ignoredItems = [];
89+
90+
this.tabBar = React.createRef();
91+
92+
this._itemsWidths = _.times(React.Children.count(this.props.children), () => null);
93+
this._indicatorOffset = new ReanimatedObject({duration: 300, easing: Easing.bezier(0.23, 1, 0.32, 1)});
94+
this._indicatorWidth = new ReanimatedObject({duration: 300, easing: Easing.bezier(0.23, 1, 0.32, 1)});
95+
this._indicatorTransitionStyle = {
96+
width: this._indicatorWidth.value,
97+
left: this._indicatorOffset.value,
98+
};
99+
100+
this.state = {
101+
itemsWidths: undefined,
102+
};
103+
113104
React.Children.toArray(this.props.children).forEach((child, index) => {
114105
if (child.props.ignore) {
115106
ignoredItems.push(index);
@@ -158,14 +149,19 @@ class TabBar extends PureComponent {
158149
}
159150

160151
renderSelectedIndicator() {
152+
// return null;
161153
const {itemsWidths} = this.state;
162154
const {indicatorStyle} = this.props;
163155
if (itemsWidths) {
164-
const transitionStyle = {
165-
width: this.runTiming(this._indicatorWidth, this._prevIndicatorWidth, 300),
166-
left: this.runTiming(this._offset, this._prevOffset, 400),
167-
};
168-
return <Reanimated.View style={[styles.selectedIndicator, indicatorStyle, transitionStyle]} />;
156+
// const transitionStyle = {
157+
// width: this.runTiming(this._indicatorWidth, this._prevIndicatorWidth, 300),
158+
// left: this.runTiming(this._offset, this._prevOffset, 400),
159+
// };
160+
return (
161+
<Reanimated.View
162+
style={[styles.selectedIndicator, indicatorStyle /* , transitionStyle */, this._indicatorTransitionStyle]}
163+
/>
164+
);
169165
}
170166
}
171167

@@ -181,7 +177,12 @@ class TabBar extends PureComponent {
181177
activeBackgroundColor,
182178
} = this.props;
183179
if (!_.isEmpty(itemStates)) {
184-
return React.Children.map(this.props.children, (child, index) => {
180+
181+
if (this.tabBarItems) {
182+
return this.tabBarItems;
183+
}
184+
185+
this.tabBarItems = React.Children.map(this.props.children, (child, index) => {
185186
return React.cloneElement(child, {
186187
labelColor,
187188
selectedLabelColor,
@@ -197,6 +198,7 @@ class TabBar extends PureComponent {
197198
onLayout: this.onItemLayout,
198199
});
199200
});
201+
return this.tabBarItems;
200202
}
201203
}
202204

@@ -210,8 +212,8 @@ class TabBar extends PureComponent {
210212
ref={this.tabBar}
211213
horizontal
212214
showsHorizontalScrollIndicator={false}
213-
style={{backgroundColor: Colors.white}}
214-
contentContainerStyle={{minWidth: containerWidth}}
215+
style={styles.tabBarScroll}
216+
contentContainerStyle={styles.tabBarScrollContent}
215217
>
216218
<View style={[styles.tabBar, height && {height}]}>{this.renderTabBarItems()}</View>
217219
{this.renderSelectedIndicator()}
@@ -222,14 +224,23 @@ class TabBar extends PureComponent {
222224
const indicatorInset = Spacings.s4;
223225

224226
return block([
227+
// calc indicator current width
225228
..._.map(itemsWidths, (width, index) => {
226229
return cond(eq(currentPage, index), [
227-
set(this._indicatorWidth, sub(itemsWidths[index], indicatorInset * 2)),
230+
set(this._indicatorWidth.nextValue, sub(itemsWidths[index], indicatorInset * 2)),
228231
]);
229232
}),
233+
// calc indicator current position
230234
..._.map(itemsOffsets, (offset, index) => {
231-
return cond(eq(currentPage, index), [set(this._offset, add(itemsOffsets[index], indicatorInset))]);
235+
return cond(eq(currentPage, index), [
236+
set(this._indicatorOffset.nextValue, add(itemsOffsets[index], indicatorInset)),
237+
]);
232238
}),
239+
240+
// Offset transition
241+
this._indicatorOffset.getTransitionBlock(),
242+
// Width transition
243+
this._indicatorWidth.getTransitionBlock(),
233244
]);
234245
}}
235246
</Code>
@@ -247,6 +258,12 @@ const styles = StyleSheet.create({
247258
flexDirection: 'row',
248259
justifyContent: 'space-between',
249260
},
261+
tabBarScroll: {
262+
backgroundColor: Colors.white,
263+
},
264+
tabBarScrollContent: {
265+
minWidth: Constants.screenWidth,
266+
},
250267
tab: {
251268
flex: 1,
252269
alignItems: 'center',
@@ -266,13 +283,13 @@ const styles = StyleSheet.create({
266283
shadowColor: Colors.dark10,
267284
shadowOpacity: 0.05,
268285
shadowRadius: 2,
269-
shadowOffset: {height: 6, width: 0}
286+
shadowOffset: {height: 6, width: 0},
270287
},
271288
android: {
272289
elevation: 5,
273-
backgroundColor: Colors.white
274-
}
275-
})
290+
backgroundColor: Colors.white,
291+
},
292+
}),
276293
},
277294
});
278295

0 commit comments

Comments
 (0)