学习内容
- 类目Icon功能
- 底部猜你喜欢
- 上拉加载更多
- 下拉刷新
- 尾部视图处理
- 自定义顶部标签导航栏
根据网络数据结构,定义数据模型
//header icon 模型
export interface HeaderItemIconModel
{
title: string,
coverPath: string,
darkCoverPath: string,
properties: HeaderItemIconUrlModel,
}
//header Icon 模型 的 properties 模型
export interface HeaderItemIconUrlModel
{
uri: string,
}
类目Icon组件封装
- 在
src/pages/Home/
创建icon.tsx
interface IProps {
iconList: HeaderItemIconModel[],
}
class Icon extends React.Component<IProps> {
renderItem = ({ item }: { item: HeaderItemIconModel }, parallaxProps?: AdditionalParallaxProps) => {
return (
<View style={styles.item}>
<Image source={{uri: item.coverPath}} style={styles.itemImage}/>
<Text style={styles.itemTitle} numberOfLines={1}>
{item.title}
</Text>
</View>
);
}
render() {
const { iconList } = this.props;
if (iconList != null && iconList.length > 0) {
return (
<View style={styles.container}>
{/* <Text>{JSON.stringify(iconList)}</Text> */}
<FlatList
data={iconList}
renderItem={this.renderItem}
numColumns={5}
/>
</View>
);
}
else {
return null;
}
}
}
const styles = StyleSheet.create({
container: {
// backgroundColor: '#fff',
borderRadius: 8,
marginTop: 5,
margin: 16,
},
item: {
flex : 1,
marginHorizontal: 5,
marginVertical: 6,
},
itemImage: {
width: '100%',
height: 70,
borderRadius: 50,
marginBottom: 5,
},
itemTitle: {
textAlign: 'center',
fontSize: 14,
color: '#333333',
marginBottom: 5,
}
});
export default Icon;
- 在
src/pages/Home/Home.tsx
调用
render() {
...
var icons: HeaderItemIconModel[] = [];
header.forEach(element => {
...
else if (element.item.moduleType == 'square' && element.item.list.length > 0) {
//广告Icon
icons = element.item.list;
}
});
return (
<View>
...
<Icon iconList={icons}/>
</View>
);
}
ps: 当然样式上实现了,但是点击事件都没有做
- 封装点击事件组件,在
src/components/Touchable.tsx
import React from "react";
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
const Touchable: React.FC<TouchableOpacityProps>= (props) => (
<TouchableOpacity activeOpacity={0.8} {...props} />
)
export default Touchable;
- 重新封装类目Icon的 item
return (
<Touchable
style={styles.item}
onPress={() => {
Alert.alert(item.properties.uri);
}}
>
...
</Touchable>
);
底部猜你喜欢模块
- 封装猜你喜欢模块
src/pages/Home/Guess.tsx
interface IProps {
data: HomeBodyItemModel;
}
class GuessItem extends React.Component<IProps> {
render() {
const { data } = this.props;
// console.log("_________:", data.intro);
return (
<Touchable
style={styles.container}
onPress={() => {
Alert.alert(data.title);
}}
>
<Image source={{ uri: data.coverPath }} style={styles.image} />
<View>
<Text style={styles.title}>{data.title}</Text>
<Text style={styles.subTitle} numberOfLines={1}>{data.intro}</Text>
<View style={styles.playContainer}>
<IconBofang2 color='#999999' />
<Text style={styles.playNum}>{numberChange(data.playsCounts)}</Text>
</View>
</View>
<IconClose color='#999999' style={styles.close} />
</Touchable>
);
}
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginLeft: 15,
marginRight: 15,
marginBottom: 10,
// backgroundColor: '#ffffff',
},
image: {
width: 80,
height: 80,
borderRadius: 12,
backgroundColor: '#dedede',
},
title: {
marginTop: 10,
marginLeft: 10,
fontSize: 16,
fontWeight: 'bold',
},
subTitle: {
marginLeft: 10,
marginTop: 10,
marginRight: 5,
width: viewPortWidth - 165,
},
playContainer: {
flexDirection: 'row',
marginLeft: 6,
// backgroundColor: 'red',
marginTop: 10,
},
playNum: {
marginLeft: 2,
color: '#999999',
},
close: {
marginTop: 10,
}
});
export default GuessItem;
- 调用封装的猜你喜欢模块
src/pages/Home/Home.tsx
renderItem = ({ item }: { item: HomeBodyModel }, parallaxProps?: AdditionalParallaxProps) => {
return (
<GuessItem data={item.item} />
);
}
全屏滚动
- 组件抽取
get header() {
const { header, body } = this.props;
var banner: HeaerItemBannerModel[] = [];
var icons: HeaderItemIconModel[] = [];
header.forEach(element => {
if (element.item.moduleType == 'focus' && element.item.list.length > 0) {
//banner广告
element.item.list.forEach((ele: HeaerItemBanner) => {
banner = ele.data;
});
}
else if (element.item.moduleType == 'square' && element.item.list.length > 0) {
//广告Icon
icons = element.item.list;
}
});
return (
<View>
<Banner banner={banner} />
<Icon iconList={icons} />
{/* <GuessTitle guessData={body} /> */}
</View>
);
}
get footer() {
return (
<View>
<Text>加载中...</Text>
</View>
);
}
get empty() {
return (
<View>
<Text style={styles.textStyle}>正在加载中...</Text>
</View>
);
}
- 将 猜你喜欢前面的模块整合到FlatList组件的头部
return (
<FlatList
ListHeaderComponent={this.header}
ListFooterComponent={this.footer}
numColumns={1}
ListEmptyComponent={this.empty}
data={body} renderItem={this.renderItem}
>
</FlatList>
);
上拉加载更多
修改src/pages/Home/Home.tsx
- FlatList设置属性onEndReached
onEndReached={this.onEndReached}
onEndReachedThreshold={0.2} //比例:距离内容底部多远开始掉接口
- 加载更多方法实现
//加载更多
onEndReached = () => {
console.log("加载更多");
const { dispatch, loading, hasMore } = this.props
if (loading || !hasMore) return;
dispatch({
type: "home/fetchGuess",
payload: {
loadMore: true,
},
});
}
修改src/models/http.ts
,添加接口调用
*fetchGuess({ callback, payload }, { call, put, select }) {
const home = yield select((state: RootState) => state.home);
let page = 1;
if (payload && payload.loadMore) {
page = home.pagenation.current + 1;
}
const { data, pagenation } = yield call(axios.get, GUESS_PAGE_URL, {
params: {
page,
}
});
let newData = data;
if (payload && payload.loadMore) {
newData = home.body.concat(newData);
}
console.log("___________数据个数:", newData.length);
yield put({
type: 'setState',
payload: {
body: newData,
pagenation: {
current: pagenation.current,
total: pagenation.total,
hasMore: newData.length < pagenation.total,
}
}
});
if (typeof callback == 'function') {
callback();
}
},
},
下拉刷新
修改src/pages/Home/Home.tsx
- FlatList设置属性onRefresh 和 refreshing
interface IState {
refreshing: boolean;
}
/**
* 首页类
*/
class Home extends React.Component<IProps, IState> {
state = {
refreshing: false,
};
...
//下拉加载更多
onRefresh = () => {
console.log("下拉刷新");
//1.修改刷新状态为 true
this.setState({
refreshing: true,
});
//2.获取数据
const { dispatch } = this.props;
dispatch({
type: "home/fetchHome",
//3.修改刷新状态为false
callback: () => {
this.setState({
refreshing: false,
})
}
});
}
...
render() {
const { body } = this.props;
const { refreshing } = this.state;
return (
<FlatList
...
onRefresh={this.onRefresh}
refreshing={refreshing}
...
>
</FlatList>
);
}
}
底部视图处理
- 处理加载更多 和 到底了
get footer() {
const { body, hasMore, loading } = this.props;
if (!hasMore) {
return (
<View style={styles.footerView}>
<Text style={styles.footerStyle}>--- 我是有底线的 ---</Text>
</View>
);
}
if (loading && hasMore && body.length > 0) {
return (
<View style={styles.footerView}>
<Text style={styles.footerLoadingStyle}>正在加载中...</Text>
</View>
);
}
}
赋值到 FlatList 的 ListFooterComponent
导航栏顶部自定义标签
首页沉浸式头部
-
将头部导航隐藏,修改
BottomTabs.tsx
componentDidMount() { this.setOptions(); } componentDidUpdate() { this.setOptions(); } setOptions = () => { const { navigation, route } = this.props; //使用route.state的时候:官方提示用getFocusedRouteNameFromRoute const routeName = getFocusedRouteNameFromRoute(route) ?? 'HomeTabs'; if (routeName === 'HomeTabs') { navigation.setOptions({ //头部透明 headerTransparent: true, headerTitle: '', }); } else { navigation.setOptions({ //展示头部样式 headerTransparent: false, headerTitle: getHeaderTitle(routeName), }); } }
封装顶部top导航栏
-
新增
pages/views/TopTabBarWrapper.tsx
import { MaterialTopTabBar, MaterialTopTabBarProps } from "@react-navigation/material-top-tabs"; import React from "react"; import { View } from "react-native"; interface IProps extends MaterialTopTabBarProps { } class TopTabBarWrapper extends React.Component<IProps> { render() { const { props } = this; return ( <View> <MaterialTopTabBar {...props} /> </View> ); } } export default TopTabBarWrapper;
-
iPhone X 之后的 全面屏 状态栏高度 计算插件
react-native-iphone-x-helper
堆栈式导航器依赖了这个插件。查看node_modules里面是否已经依赖。-
如果未依赖,就安装一下
yarn add react-native-iphone-x-helper
-
如果依赖,直接使用
//... import { getStatusBarHeight } from 'react-native-iphone-x-helper'; class TopTabBarWrapper extends React.Component<IProps> { render() { //... return ( <View style={styles.container}> //... </View> ); } } const styles = StyleSheet.create({ container: { back paddingTop: getStatusBarHeight(), } }); export default TopTabBarWrapper;
-
导航栏布局
-
背景色修改 6.x 版本导航栏背景色修改放到了
screenOptions
,MaterialTopTabBar
没有更多的属性设置//修改导航栏上的背景色 tabBarStyle: { backgroundColor: 'transparent', },
-
分类,搜索框(语音,搜索按钮),历史记录
class TopTabBarWrapper extends React.Component<IProps> { render() { const { props } = this; return ( <View style={styles.container}> <View style={styles.topTabBarView}> <View style={styles.tabbar}> <MaterialTopTabBar {...props} /> </View> <Touchable style={styles.categoryBtn}> <IconFenlei color='#ff6600' style={styles.categoryImage} /> </Touchable> </View> <View style={styles.bottom}> <Touchable style={styles.searchBtn}> <Text style={styles.searchText}>搜索按钮</Text> <Touchable style={styles.yuyinBtn}> <IconYuyin color='#555555'/> </Touchable> <View style={styles.line}></View> <Touchable style={styles.searchIconBtn}> <IconSousuo color='#ff5500'/> </Touchable> </Touchable> <Touchable style={styles.historyBtn}> <IconLishi color='#999999' /> </Touchable> </View> </View> ); } }
导航栏背景渐变
-
安装插件
yarn add react-native-linear-gradient # 需要原生支持 ./pod.sh
-
渐变色动画组件
yarn add react-native-linear-animated-gradient-transition
-
添加全局状态属性
activeBannerIndex: number, //当前轮播图的下标 gradientVisible: boolean, // 渐变色组件是否显示的状态
-
改造
src/navigator/HomeTabs.tsx
拿到 渐变组件的显示状态,来修改导航栏的样式import React from "react"; import { createMaterialTopTabNavigator, MaterialTopTabBarProps } from "@react-navigation/material-top-tabs"; import Home from "@/pages/Home/Home"; import TopTabBarWrapper from "@/pages/views/TopTabBarWrapper"; import { StyleSheet } from "react-native"; import { RootState } from "../models"; import { connect, ConnectedProps } from "react-redux"; //声明变量,接收函数返回值 const Tab = createMaterialTopTabNavigator(); const mapStateToProps = ({home}: RootState) => { return { gradientVisible: home.gradientVisible, }; }; const connector = connect(mapStateToProps); type ModelState = ConnectedProps<typeof connector>; interface IProps extends ModelState {} class HomeTabs extends React.Component<IProps> { renderTabBar = (props: MaterialTopTabBarProps) => { return ( <TopTabBarWrapper {...props} /> ); } render() { const { gradientVisible } = this.props; return ( // {/* 'tabBarOptions' is deprecated. Migrate the options to 'screenOptions' instead. */} <Tab.Navigator tabBar={this.renderTabBar} screenOptions={{ //... //tab底部横条样式 tabBarIndicatorStyle: { //... backgroundColor: gradientVisible ? '#fff' : '#f86442', }, //... tabBarActiveTintColor: gradientVisible ? '#fff' : '#f86442', }} > //... </Tab.Navigator> ); } } const styles = StyleSheet.create({ sceneContainer: { backgroundColor: 'transparent', } }); export default connector(HomeTabs);
-
改造 banner广告组件
src/pages/Home/Banner.tsx
,更新banner轮播的当前索引值import { RootState } from "@/models/index"; import { HeaerItemBannerModel } from "@/models/home"; import { hp, viewPortWidth, wp } from "@/utils/index"; import React from "react"; import { Platform, StyleSheet, View } from "react-native"; import SnapCarousel, { AdditionalParallaxProps, Pagination, ParallaxImage } from "react-native-snap-carousel"; import { connect, ConnectedProps } from "react-redux"; //... const mapStateToProps = ({ home }: RootState) => { return { activeBannerIndex: home.activeBannerIndex, }; }; const connector = connect(mapStateToProps); type ModelState = ConnectedProps<typeof connector>; interface IProps extends ModelState { banner: HeaerItemBannerModel[], } class Banner extends React.Component<IProps> { //更新 banner 轮播的索引值 onSnapToItem = (index: number) => { const { dispatch } = this.props; dispatch({ type: 'home/setState', payload: { activeBannerIndex: index, }, }); } //... render() { //... } } // 定义样式 const styles = StyleSheet.create({ //... }) export default connector(Banner);
-
改造首页
/src/pages/Home/Home.tsx
, 监听FlatList的 onScroll,来计算出 FlatList滚动的偏移量,来计算是否需要展示渐变图层//... //拿到 models 中 home的 num 值 const mapStateToProps = ({ home, loading }: RootState) => ({ header: home.header, body: home.body, hasMore: home.pagenation.hasMore, loading: loading.effects['home/fetchGuess'], gradientVisible: home.gradientVisible, //渐变色图层显示状态 }); //获取到函数 const connector = connect(mapStateToProps); type MadelState = ConnectedProps<typeof connector>; interface IProps extends MadelState { navigation: RootStackNavigation; } interface IState { refreshing: boolean; } /** * 首页类 */ class Home extends React.Component<IProps, IState> { //... onScroll = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => { const offSetY = nativeEvent.contentOffset.y; // console.log("___________offSetY:", offSetY); // console.log("___________sideWidth:", sideHeight); let newGradientVisible = offSetY < sideHeight; const {dispatch, gradientVisible} = this.props; if (gradientVisible !== newGradientVisible) { dispatch({ type: "home/setState", payload: { gradientVisible: newGradientVisible, }, }); } } //... render() { return ( <FlatList //... onScroll={this.onScroll} > </FlatList> ); } } const styles = StyleSheet.create({ //... }); // 导出首页类 export default connector(Home);
-
顶部自定义标签组件的优化
/src/pages/views/TopTabBarWrapper.tsx
,- 根据banner轮播的索引值获取渐变色数组,返回LinearAnimatedGradientTransition图层
- 根据是否展示渐变色图层,修改顶部自定义标签子控件的眼神
//... const mapStateToProps = ({ home }: RootState) => { return ({ //索引值 activeBannerIndex: home.activeBannerIndex, //渐变层是否展示的状态 gradientVisible: home.gradientVisible, header: home.header, }); } const connector = connect(mapStateToProps); type ModelState = ConnectedProps<typeof connector>; interface IProps extends MaterialTopTabBarProps, ModelState { } class TopTabBarWrapper extends React.Component<IProps> { get linearGradient() { const { activeBannerIndex, gradientVisible, header } = this.props; var banner: HeaerItemBannerModel[] = []; header.forEach(element => { if (element.item.moduleType == 'focus' && element.item.list.length > 0) { //banner广告 element.item.list.forEach((ele: HeaerItemBanner) => { banner = ele.data; }); } }); //根据banner轮播的索引值获取渐变色数组,返回LinearAnimatedGradientTransition图层 var linearColors = banner[activeBannerIndex] ? banner[activeBannerIndex].colors : ['#fff', '#ff5500']; if (gradientVisible) { // console.log("_______:",activeBannerIndex); // console.log("________:来了",linearColors); return ( <LinearAnimatedGradientTransition colors={linearColors} style={styles.grandient} /> ); } return null; } render() { const { gradientVisible, ...restProps } = this.props; let textStyle = styles.searchText; let lineStyle = styles.line; let yuyinColor = '#555'; let historyColor = '#999'; let searchColor = '#ff5500'; let categoryColor = searchColor; //根据是否展示渐变色图层,修改顶部自定义标签子控件的眼神 if (gradientVisible) { textStyle = styles.whiteText; yuyinColor = historyColor = searchColor = categoryColor = '#fff'; lineStyle = styles.whiteLine; } return ( <View style={styles.container}> {this.linearGradient} <View style={styles.topTabBarView}> <View style={styles.tabbar}> <MaterialTopTabBar {...restProps} /> </View> <Touchable style={styles.categoryBtn}> <IconFenlei color={categoryColor} style={styles.categoryImage} /> </Touchable> </View> <View style={styles.bottom}> <Touchable style={styles.searchBtn}> <Text style={textStyle}>搜索按钮</Text> <Touchable style={styles.yuyinBtn}> <IconYuyin color={yuyinColor} /> </Touchable> <View style={lineStyle}></View> <Touchable style={styles.searchIconBtn}> <IconSousuo color={searchColor} /> </Touchable> </Touchable> <Touchable style={styles.historyBtn}> <IconLishi color={historyColor} /> </Touchable> </View> </View> ); } } const styles = StyleSheet.create({ //... }); export default connector(TopTabBarWrapper);
ps:一步一个脚印👣,up~