Skip to content
This repository was archived by the owner on Nov 26, 2019. It is now read-only.

Commit 9db82ec

Browse files
committed
Initial work on UIKit style animation for header
1 parent 5d25053 commit 9db82ec

3 files changed

Lines changed: 133 additions & 34 deletions

File tree

src/components/Header/HeaderAnimated.tsx

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,75 @@ type Props<T extends Route> = {
2929
style?: StyleProp<ViewStyle>;
3030
};
3131

32-
const { interpolate, multiply } = Animated;
32+
const { interpolate, add } = Animated;
33+
34+
const UIKitPreset: HeaderAnimationPreset = {
35+
styleInterpolator: ({ current, next, layout }: InterpolationProps) => {
36+
/**
37+
* NOTE: this offset calculation is an approximation that gives us
38+
* decent results in many cases, but it is ultimately a poor substitute
39+
* for text measurement. See the comment on title for more information.
40+
*
41+
* - 70 is the width of the left button area.
42+
* - 25 is the width of the left button icon (to account for label offset)
43+
*/
44+
const buttonAreaSize = 70;
45+
const buttonIconSize = 25;
46+
const titleOffset = layout.width / 2 - buttonAreaSize + buttonIconSize;
47+
const backTitleOffset = layout.width / 2 - buttonAreaSize - buttonIconSize;
48+
49+
const progress = add(current, next ? next : 0);
50+
51+
return {
52+
leftButtonStyle: {
53+
opacity: interpolate(progress, {
54+
inputRange: [0, 1, 2],
55+
outputRange: [0, 1, 0],
56+
}),
57+
},
58+
backTitleStyle: {
59+
opacity: interpolate(progress, {
60+
inputRange: [0.7, 1, 1.3],
61+
outputRange: [0, 1, 0],
62+
}),
63+
transform: [
64+
{
65+
translateX: interpolate(progress, {
66+
inputRange: [0, 1, 2],
67+
outputRange: [backTitleOffset, 0, -backTitleOffset],
68+
}),
69+
},
70+
],
71+
},
72+
titleStyle: {
73+
opacity: interpolate(progress, {
74+
inputRange: [0.5, 1, 1.7],
75+
outputRange: [0, 1, 0],
76+
}),
77+
transform: [
78+
{
79+
translateX: interpolate(progress, {
80+
inputRange: [0, 1, 2],
81+
outputRange: [titleOffset, 0, -titleOffset],
82+
}),
83+
},
84+
],
85+
},
86+
};
87+
},
88+
};
3389

3490
const FadePreset: HeaderAnimationPreset = {
3591
styleInterpolator: ({ current, next }: InterpolationProps) => {
36-
const progress = next
37-
? multiply(
38-
current,
39-
interpolate(next, {
40-
inputRange: [0, 1],
41-
outputRange: [1, 0],
42-
})
43-
)
44-
: current;
92+
const progress = add(current, next ? next : 0);
93+
const opacity = interpolate(progress, {
94+
inputRange: [0, 1, 2],
95+
outputRange: [0, 1, 0],
96+
});
4597

4698
return {
47-
leftButtonStyle: { opacity: progress },
48-
titleStyle: { opacity: progress },
99+
leftButtonStyle: { opacity },
100+
titleStyle: { opacity },
49101
};
50102
},
51103
};
@@ -54,7 +106,7 @@ export default class HeaderAnimated<T extends Route> extends React.Component<
54106
Props<T>
55107
> {
56108
static defaultProps = {
57-
preset: FadePreset,
109+
preset: Platform.OS === 'ios' ? UIKitPreset : FadePreset,
58110
};
59111

60112
render() {

src/components/Header/HeaderAnimatedItem.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ export type InterpolationProps = {
1515
export type StyleInterpolator = (
1616
props: InterpolationProps
1717
) => {
18-
leftButtonStyle: any;
19-
titleStyle: any;
18+
backTitleStyle?: any;
19+
leftButtonStyle?: any;
20+
titleStyle?: any;
2021
};
2122

2223
export type HeaderAnimationPreset = {
@@ -62,7 +63,11 @@ export default class HeaderAnimatedItem<
6263
style,
6364
} = this.props;
6465

65-
const { titleStyle, leftButtonStyle } = this.getInterpolatedStyle(
66+
const {
67+
titleStyle,
68+
leftButtonStyle,
69+
backTitleStyle,
70+
} = this.getInterpolatedStyle(
6671
preset.styleInterpolator,
6772
layout,
6873
scene.progress,
@@ -73,7 +78,11 @@ export default class HeaderAnimatedItem<
7378
<View style={[styles.content, style]}>
7479
{previous ? (
7580
<Animated.View style={[styles.left, leftButtonStyle]}>
76-
<HeaderBackButton onPress={onGoBack} title={previous.title} />
81+
<HeaderBackButton
82+
onPress={onGoBack}
83+
title={previous.title}
84+
titleStyle={backTitleStyle}
85+
/>
7786
</Animated.View>
7887
) : null}
7988
<HeaderTitle style={[previous ? styles.title : null, titleStyle]}>

src/components/Header/HeaderBackButton.tsx

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import * as React from 'react';
22
import {
33
I18nManager,
44
Image,
5-
Text,
65
View,
76
Platform,
87
StyleSheet,
98
LayoutChangeEvent,
10-
StyleProp,
11-
TextStyle,
9+
Text,
10+
MaskedViewIOS,
1211
} from 'react-native';
13-
12+
import Animated from 'react-native-reanimated';
1413
import TouchableItem from '../TouchableItem';
1514

1615
type Props = {
@@ -26,12 +25,12 @@ type Props = {
2625
truncatedTitle?: string | null;
2726
backTitleVisible?: boolean;
2827
allowFontScaling?: boolean;
29-
titleStyle?: StyleProp<TextStyle>;
28+
titleStyle?: React.ComponentProps<typeof Text>['style'];
3029
width?: number;
3130
};
3231

3332
type State = {
34-
initialTextWidth?: number;
33+
initialTitleWidth?: number;
3534
};
3635

3736
class HeaderBackButton extends React.Component<Props, State> {
@@ -48,11 +47,12 @@ class HeaderBackButton extends React.Component<Props, State> {
4847
state: State = {};
4948

5049
private handleTextLayout = (e: LayoutChangeEvent) => {
51-
if (this.state.initialTextWidth) {
50+
if (this.state.initialTitleWidth) {
5251
return;
5352
}
53+
5454
this.setState({
55-
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
55+
initialTitleWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
5656
});
5757
};
5858

@@ -83,7 +83,7 @@ class HeaderBackButton extends React.Component<Props, State> {
8383
private getTitleText = () => {
8484
const { width, title, truncatedTitle } = this.props;
8585

86-
let { initialTextWidth } = this.state;
86+
let { initialTitleWidth: initialTextWidth } = this.state;
8787

8888
if (title === null) {
8989
return null;
@@ -103,22 +103,41 @@ class HeaderBackButton extends React.Component<Props, State> {
103103
titleStyle,
104104
tintColor,
105105
} = this.props;
106+
const { initialTitleWidth: titleWidth } = this.state;
107+
106108
let backTitleText = this.getTitleText();
107109

108110
if (!backTitleVisible || backTitleText === null) {
109111
return null;
110112
}
111113

112114
return (
113-
<Text
114-
accessible={false}
115-
onLayout={this.handleTextLayout}
116-
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
117-
numberOfLines={1}
118-
allowFontScaling={!!allowFontScaling}
115+
<MaskedViewIOS
116+
maskElement={
117+
<View style={styles.iconMaskContainer}>
118+
<Image
119+
source={require('../../assets/back-icon-mask.png')}
120+
style={styles.iconMask}
121+
/>
122+
<View style={styles.iconMaskFillerRect} />
123+
</View>
124+
}
119125
>
120-
{this.getTitleText()}
121-
</Text>
126+
<Animated.Text
127+
accessible={false}
128+
onLayout={this.handleTextLayout}
129+
style={[
130+
styles.title,
131+
tintColor ? { color: tintColor } : null,
132+
titleWidth ? { paddingRight: titleWidth } : null,
133+
titleStyle,
134+
]}
135+
numberOfLines={1}
136+
allowFontScaling={!!allowFontScaling}
137+
>
138+
{this.getTitleText()}
139+
</Animated.Text>
140+
</MaskedViewIOS>
122141
);
123142
}
124143

@@ -173,6 +192,7 @@ const styles = StyleSheet.create({
173192
},
174193
title: {
175194
fontSize: 17,
195+
letterSpacing: 0.25,
176196
paddingRight: 10,
177197
},
178198
icon: Platform.select({
@@ -200,6 +220,24 @@ const styles = StyleSheet.create({
200220
marginRight: 6,
201221
}
202222
: {},
223+
iconMaskContainer: {
224+
flex: 1,
225+
flexDirection: 'row',
226+
justifyContent: 'center',
227+
},
228+
iconMaskFillerRect: {
229+
flex: 1,
230+
backgroundColor: '#000',
231+
},
232+
iconMask: {
233+
height: 21,
234+
width: 13,
235+
marginLeft: -14.5,
236+
marginVertical: 12,
237+
alignSelf: 'center',
238+
resizeMode: 'contain',
239+
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
240+
},
203241
});
204242

205243
export default HeaderBackButton;

0 commit comments

Comments
 (0)