Skip to content

Commit 91a37c4

Browse files
committed
分类页面优化
1 parent 1b9118e commit 91a37c4

File tree

7 files changed

+339
-27
lines changed

7 files changed

+339
-27
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
"moment": "^2.29.1",
2929
"react": "16.13.1",
3030
"react-native": "0.63.3",
31-
"react-native-animated-header": "^1.0.7",
3231
"react-native-config": "^1.4.1",
3332
"react-native-fast-image": "^8.3.4",
3433
"react-native-gesture-handler": "^1.9.0",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import Header from './Header';
4+
5+
type Props = {
6+
style?: any,
7+
backText?: string,
8+
title?: string,
9+
renderLeft?: () => React.ComponentType<any> | React.ReactElement | null,
10+
renderRight?: () => React.ComponentType<any> | React.ReactElement | null,
11+
backStyle?: any,
12+
backTextStyle?: any,
13+
titleStyle?: any,
14+
toolbarColor?: string,
15+
headerMaxHeight?: number,
16+
disabled?: boolean,
17+
noBorder?: boolean,
18+
parallax?: boolean,
19+
imageSource?: any,
20+
};
21+
22+
export default class AnimatedHeader extends React.PureComponent<Props> {
23+
_onScroll = (e) => {
24+
this.header.onScroll(e);
25+
};
26+
27+
render() {
28+
const arr = React.Children.toArray(this.props.children);
29+
if (arr.length === 0) {
30+
console.error(
31+
'AnimatedHeader must have ScrollView or FlatList as a child',
32+
);
33+
}
34+
if (arr.length > 1) {
35+
console.error('Invalid child, only 1 child accepted');
36+
}
37+
const {headerMaxHeight} = this.props;
38+
const child = React.cloneElement(arr[0], {
39+
style: {flex: 1},
40+
ref: (r) => (this.scrollView = r),
41+
scrollEventThrottle: 16,
42+
onScroll: this._onScroll,
43+
contentContainerStyle: {paddingTop: headerMaxHeight || 200},
44+
});
45+
return (
46+
<View style={this.props.style}>
47+
{child}
48+
<Header
49+
{...this.props}
50+
ref={(r) => {
51+
this.header = r;
52+
}}
53+
/>
54+
</View>
55+
);
56+
}
57+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import React from 'react';
2+
import {
3+
Animated,
4+
Platform,
5+
StyleSheet,
6+
View,
7+
TouchableOpacity,
8+
} from 'react-native';
9+
10+
import {isIphoneX} from 'react-native-iphone-x-helper';
11+
12+
const ios = Platform.OS === 'ios';
13+
const isIphoneX_ = isIphoneX();
14+
const iphoneXTopInset = 24;
15+
const initToolbarHeight = ios ? 46 : 56;
16+
17+
const paddingTop = ios ? 18 : 0;
18+
const topInset = isIphoneX_ ? iphoneXTopInset : 0;
19+
20+
const toolbarHeight = initToolbarHeight + topInset + paddingTop;
21+
22+
export default class Header extends React.PureComponent {
23+
constructor(props) {
24+
super(props);
25+
this.headerHeight = props.headerMaxHeight;
26+
this.state = {
27+
scrollOffset: new Animated.Value(0),
28+
left: 0,
29+
bottom: 0,
30+
};
31+
}
32+
33+
onScroll = (e) => {
34+
if (this.props.disabled) {
35+
return;
36+
}
37+
this.state.scrollOffset.setValue(e.nativeEvent.contentOffset.y);
38+
};
39+
40+
onBackLayout = (e) => {
41+
const layout = e.nativeEvent.layout;
42+
const bottom =
43+
toolbarHeight - layout.y - layout.height - paddingTop - topInset;
44+
this.setState({bottom: bottom, left: e.nativeEvent.layout.x});
45+
};
46+
47+
_getFontSize = () => {
48+
const {scrollOffset} = this.state;
49+
const backFontSize =
50+
this.props.backTextStyle.fontSize ||
51+
Header.defaultProps.backTextStyle.fontSize;
52+
const titleFontSize =
53+
this.props.titleStyle.fontSize || Header.defaultProps.titleStyle.fontSize;
54+
return scrollOffset.interpolate({
55+
inputRange: [0, this.headerHeight - toolbarHeight],
56+
outputRange: [titleFontSize, backFontSize],
57+
extrapolate: 'clamp',
58+
});
59+
};
60+
61+
_getLeft = () => {
62+
const {scrollOffset} = this.state;
63+
const left =
64+
this.props.titleStyle.left || Header.defaultProps.titleStyle.left;
65+
return scrollOffset.interpolate({
66+
inputRange: [0, this.headerHeight - toolbarHeight],
67+
outputRange: [left, this.state.left],
68+
extrapolate: 'clamp',
69+
});
70+
};
71+
72+
_getHeight = () => {
73+
const {scrollOffset} = this.state;
74+
return scrollOffset.interpolate({
75+
inputRange: [0, this.headerHeight - toolbarHeight],
76+
outputRange: [this.headerHeight, toolbarHeight],
77+
extrapolate: 'clamp',
78+
});
79+
};
80+
81+
_getBottom = () => {
82+
const {scrollOffset} = this.state;
83+
const bottom =
84+
this.props.titleStyle.bottom || Header.defaultProps.titleStyle.bottom;
85+
return scrollOffset.interpolate({
86+
inputRange: [0, this.headerHeight - toolbarHeight],
87+
outputRange: [bottom, this.state.bottom],
88+
extrapolate: 'clamp',
89+
});
90+
};
91+
92+
_getOpacity = () => {
93+
const {scrollOffset} = this.state;
94+
return this.props.backText
95+
? scrollOffset.interpolate({
96+
inputRange: [0, this.headerHeight - toolbarHeight],
97+
outputRange: [1, 0],
98+
extrapolate: 'clamp',
99+
})
100+
: 0;
101+
};
102+
103+
_getImageOpacity = () => {
104+
const {scrollOffset} = this.state;
105+
return this.props.imageSource
106+
? scrollOffset.interpolate({
107+
inputRange: [0, this.headerHeight - toolbarHeight],
108+
outputRange: [1, 0],
109+
extrapolate: 'clamp',
110+
})
111+
: 0;
112+
};
113+
114+
_getImageScaleStyle = () => {
115+
if (!this.props.parallax) {
116+
return undefined;
117+
}
118+
const {scrollOffset} = this.state;
119+
const scale = scrollOffset.interpolate({
120+
inputRange: [-100, -0],
121+
outputRange: [1.5, 1],
122+
extrapolate: 'clamp',
123+
});
124+
125+
return {
126+
transform: [
127+
{
128+
scale,
129+
},
130+
],
131+
};
132+
};
133+
134+
render() {
135+
const {
136+
imageSource,
137+
toolbarColor,
138+
titleStyle,
139+
onBackPress,
140+
backStyle,
141+
backTextStyle,
142+
} = this.props;
143+
const height = this._getHeight();
144+
const left = this._getLeft();
145+
const bottom = this._getBottom();
146+
const opacity = this._getOpacity();
147+
const fontSize = this._getFontSize();
148+
const imageOpacity = this._getImageOpacity();
149+
const headerStyle = this.props.noBorder
150+
? undefined
151+
: {borderBottomWidth: 1, borderColor: '#a7a6ab'};
152+
153+
return (
154+
<Animated.View
155+
style={[
156+
styles.header,
157+
headerStyle,
158+
{
159+
height: height,
160+
backgroundColor: toolbarColor,
161+
},
162+
]}>
163+
{imageSource && (
164+
<Animated.Image
165+
style={[
166+
StyleSheet.absoluteFill,
167+
{width: null, height: null, opacity: imageOpacity},
168+
this._getImageScaleStyle(),
169+
]}
170+
source={imageSource}
171+
resizeMode="cover"
172+
/>
173+
)}
174+
<View style={styles.toolbarContainer}>
175+
<View style={styles.statusBar} />
176+
<View style={styles.toolbar}>
177+
{this.props.renderLeft && this.props.renderLeft()}
178+
<TouchableOpacity
179+
disabled={!onBackPress}
180+
onPress={onBackPress}
181+
activeOpacity={0.8}
182+
style={[styles.titleButton, backStyle]}
183+
onLayout={this.onBackLayout}>
184+
<Animated.Text
185+
style={[
186+
backTextStyle,
187+
{alignSelf: 'center', opacity: opacity},
188+
]}>
189+
{this.props.backText || 'Back2'}
190+
</Animated.Text>
191+
</TouchableOpacity>
192+
<View style={styles.flexView} />
193+
{this.props.renderRight && this.props.renderRight()}
194+
</View>
195+
</View>
196+
<Animated.Text
197+
style={[
198+
titleStyle,
199+
{
200+
position: 'absolute',
201+
left: left,
202+
bottom: bottom,
203+
fontSize,
204+
},
205+
]}>
206+
{this.props.title}
207+
</Animated.Text>
208+
</Animated.View>
209+
);
210+
}
211+
}
212+
213+
const styles = StyleSheet.create({
214+
toolbarContainer: {
215+
height: toolbarHeight,
216+
},
217+
statusBar: {
218+
height: topInset + paddingTop,
219+
},
220+
toolbar: {
221+
flex: 1,
222+
flexDirection: 'row',
223+
alignItems: 'center',
224+
},
225+
header: {
226+
position: 'absolute',
227+
top: 0,
228+
left: 0,
229+
right: 0,
230+
},
231+
titleButton: {
232+
flexDirection: 'row',
233+
},
234+
flexView: {
235+
flex: 1,
236+
},
237+
});
238+
239+
Header.defaultProps = {
240+
backText: '',
241+
title: '',
242+
renderLeft: undefined,
243+
renderRight: undefined,
244+
backStyle: {marginLeft: 10},
245+
backTextStyle: {fontSize: 16},
246+
titleStyle: {fontSize: 20, left: 40, bottom: 30},
247+
toolbarColor: '#FFF',
248+
headerMaxHeight: 200,
249+
disabled: false,
250+
imageSource: undefined,
251+
};

ts/navigator/Router.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import TopicDetailPage from '@/page/discover/TopicDetailPage';
2121
import CategoryDetailPage from '@/page/discover/CategoryDetailPage';
2222
import {ICategory} from '@/model/Category';
2323

24-
//定义每一个页面的名称以及进入页面传递参数的类型
24+
//定义堆栈路由参数每一个页面的名称以及进入页面传递参数的类型
2525
export type RootStackParamList = {
2626
BottomTabs: undefined;
2727
SearchPage: undefined;
@@ -45,6 +45,7 @@ export type RootNavigation = StackNavigationProp<RootStackParamList>;
4545
//创建堆栈导航器
4646
const Stack = createStackNavigator<RootStackParamList>();
4747

48+
//配置堆栈路由页面
4849
function RootStackScreen() {
4950
return (
5051
<Stack.Navigator
@@ -85,11 +86,16 @@ function RootStackScreen() {
8586
component={VideoDetailPage}
8687
options={{headerShown: false}}
8788
/>
88-
<Stack.Screen name="CategoryDetail" component={CategoryDetailPage} />
89+
<Stack.Screen
90+
name="CategoryDetail"
91+
component={CategoryDetailPage}
92+
options={{headerShown: false}}
93+
/>
8994
</Stack.Navigator>
9095
);
9196
}
9297

98+
//定义模态路由参数每一个页面的名称以及进入页面传递参数的类型
9399
export type ModalStackParamList = {
94100
Root: undefined;
95101
Gallery: {
@@ -100,8 +106,10 @@ export type ModalStackParamList = {
100106
};
101107
};
102108

109+
//导出模态路由导航参数,方便外界调用
103110
export type ModalStackNavigation = StackNavigationProp<ModalStackParamList>;
104111

112+
//创建模态路由导航
105113
const ModalStack = createStackNavigator<ModalStackParamList>();
106114

107115
function ModalStackScreen() {

0 commit comments

Comments
 (0)