小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
1.简介
学习了Flexbox模型之后,对使用rn设计一个页面已经有了一些基础,这一章我们开始尝试使用rn做一个简单的App首页,首页布局从上之下依次为:
- 状态栏:显示时间、电量等信息
- 搜索栏:输入内容、点击搜索按钮
- 广告banner:循环播放的广告等
- 商品列表:商品信息列表
\
2.布局实现
整理布局如下:
// 样式
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column'
},
search_bar: {
marginTop: 20,
height: 40,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: 2,
backgroundColor: 'red'
},
ad_banner: {
height: 180,
justifyContent: 'center',
alignItems: 'flex-start',
backgroundColor: 'green'
},
product_list: {
flex: 1,
justifyContent: 'center',
backgroundColor: 'orange'
}
});
// 布局
const TaobaoApp = () => {
return (
<View style={styles.container}>
<View style={styles.search_bar}>
<Text>搜索</Text>
</View>
<View style={styles.ad_banner}>
<Text>banner</Text>
</View>
<View style={styles.product_list}>
<Text>商品列表</Text>
</View>
</View>
);
};
export default TaobaoApp;
复制代码
- 搜索栏高度时40,即height: 40
- banner高度为180,即height: 180
- 商品列表高度为除去其他View之外填充满父View,可使用flex:1,类似Anroid中的match_parent。
效果如下:
问题:顶部会有一个多余的空白条。
由于是基于Android和iOS开发的差异,iOSUI从屏幕顶部开始,为了不与状态栏重叠,使用marginTop:20来解决,而Android默认开始的位置不包括状态栏,所以marginTop:20造成了空白区域的出现。
解决:React Native使用Platform开判断当前运行的系统,通过Platform.OS的返回值来决定marginTop的值即可,代码做如下修改:
//记得import Platform
import {View, Text, StyleSheet, Platform} from 'react-native';
// 将search_bar样式调整
marginTop: Platform.OS == 'android' ? 0 : 20,
复制代码
2.1 搜索栏
搜索栏包括一个可输入文字的文本框和一个搜索按钮,输入框可以使用TextInput,按钮使用Button,内容如下:
// 布局
const TaobaoApp = () => {
return (
<View style={styles.container}>
<View style={styles.search_bar}>
<TextInput style={styles.input} placeholder="搜索"></TextInput>
<Button
style={styles.button}
title="搜索"/>
</View>
<View style={styles.ad_banner}>
<Text>banner</Text>
</View>
<View style={styles.product_list}>
<Text>商品列表</Text>
</View>
</View>
);
};
export default TaobaoApp;
复制代码
效果如下:
2.2 banner
Android中使用ViewPager很容易就能实现,但是这是Android特有的组件,我们还是使用ScrollView来实现一个通用的banner。
2.2.1 ScrollView
ScrollView基本用法很简单,直接看一下吧:
const AdBanner = () => {
return (
<View style={styles.ad_banner}>
<ScrollView
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
>
<Text
style={{width: 100, height: 180, backgroundColor: 'red'}}>广告1</Text>
<Text
style={{width: 100, height: 180, backgroundColor: 'yellow'}}>广告2</Text>
<Text
style={{width: 100, height: 180, backgroundColor: 'blue'}}>广告3</Text>
</ScrollView>
</View>
);
};
复制代码
banner为横向滚动,设置horizontal={true},并开启分页效果pagingEnabled={true},隐藏横向的滚动条showsHorizontalScrollIndicator={false}。
效果如下:
\
2.2.2 屏幕尺寸
首先,banner的宽度为屏幕的宽度,React Native已经提供了API,使用Dimensions来获取屏幕尺寸,宽度获取:Dimensions.get('window').width,代码修改如下:
const AdBanner = () => {
return (
<View style={styles.ad_banner}>
<ScrollView
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
ref = {com => this._banner = com}
>
<Text
style={{width:Dimensions.get('window').width, height: 180, backgroundColor: 'red'}}>广告1</Text>
<Text
style={{width:Dimensions.get('window').width, height: 180, backgroundColor: 'yellow'}}>广告2</Text>
<Text
style={{width:Dimensions.get('window').width, height: 180, backgroundColor: 'blue'}}>广告3</Text>
</ScrollView>
</View>
);
};
复制代码
效果如下:
2.2.3 自动滚动
抛开rn,如果让scrollView自动滚动,可以使用一个定时器,每隔指定时间,让scrollView进行横向的位移操作。
在rn中同样可以使用这个流程,这包含几个知识点:
- 动态改变值:能动态的改变scrollView的值,使用state
- 生命周期:在控件加载后开始动画,控件卸载后停止动画
- 定时器:定时器的使用
- 图片引用:引用本地图片
我们一个一个来解决:
state状态
一个基本的state定义为 const [pageNumber, setPageNumber] = useState(0);
即创建一个变量pageNumber,并赋初始值0,创建一个改变变量的值setPageNumber。
使用定时器动态改变pageNumber即可简介控制scrollView的偏移量。
生命周期
看到的资料一般都是使用继承自React.Component的Class控件来使用生命周期,而官方文档推荐使用函数式控件,后面稍微看了一下在函数式控件中可以使用useEffect来模拟声明周期,具体是
useEffect(() => {
//模式didMount 在此编写逻辑
return () => {
//模拟willUnMount 在此编写逻辑
}
}, [])
复制代码
useEffect的第二个参数使用[],这样在函数中定义的逻辑只会在加载后执行一次,即模式didMount,在返回值中的逻辑会在卸载后执行一次,即模拟willUnMount。
定时器
使用setInterval函数即可,第一个参数是执行函数体,第二个参数是时间间隔。
图片引用
使用以下3个步骤即可:
1、创建存储图片文件的文件夹
2、创建图片文件引用
3、使用图片
import image_list from './image_list';
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img1}/>
复制代码
基于以上四点,实现如下:
const AdBanner = () => {
const [pageNumber, setPageNumber] = useState(0);
useEffect(() => {
let interval = setInterval(() => {
var nextPage = pageNumber + 1;
if (nextPage >= 3) {
nextPage = 0;
}
setPageNumber(nextPage);
const offSetX = nextPage * ScreenWidth;
this._banner.scrollTo({x: offSetX, y: 0, animated: true});
}, 3000);
return () => {
clearInterval(interval);
interval = null;
}
}, [])
return (
<View style={styles.ad_banner}>
<ScrollView
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
ref = {com => this._banner = com}
>
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img1}/>
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img2}/>
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img3}/>
</ScrollView>
</View>
);
};
复制代码
效果:
问题:图片只会切换一次就停止了?
查了一下log,useEffect中获取pageNumer的值一直是0,即使使用了setPageNumber方法也没有改变,因为我们使用的是模拟mount生命周期,useEffect只执行了一次,所以拿到的值始终是注册时的那个值,解决方法也很简单,使用另一个钩子函数获取正确的值,代码修改如下:
const AdBanner = () => {
const [pageNumber, setPageNumber] = useState(0);
const countRef = useRef(pageNumber);
useEffect(() => {
countRef.current = pageNumber;
})
useEffect(() => {
let interval = setInterval(() => {
var nextPage = countRef.current + 1;
if (nextPage >= 3) {
nextPage = 0;
}
setPageNumber(nextPage);
const offSetX = nextPage * ScreenWidth;
this._banner.scrollTo({x: offSetX, y: 0, animated: true});
}, 3000);
return () => {
clearInterval(interval);
interval = null;
}
}, [])
return (
<View style={styles.ad_banner}>
<ScrollView
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
ref = {com => this._banner = com}
>
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img1}/>
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img2}/>
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img3}/>
</ScrollView>
</View>
);
};
复制代码
这样,banner终于动起来了了,我们继续
2.3 商品列表
商品列表很容易想到使用ListView,无论iOS、Android中都有,但是尴尬的是RN中标记废弃了。。。。。
/**
* @deprecated See Flatlist or SectionList
* or use `deprecated-react-native-listview`
* @see https://fb.me/nolistview
*/
复制代码
估计是优化的不好,那咱们就使用Flatlist来实现吧。
Flatlist很简单,必须实现两个属性data和renderItem,data为数据源,render时解析数据源并渲染组件的函数,代码如下:
const Products = () => {
return (
<View style={styles.product_list}>
<FlatList
style={{flex: 1}}
data={[
{key: '商品1'},
{key: '商品2'},
{key: '商品3'},
{key: '商品4'},
{key: '商品5'},
{key: '商品6'},
{key: '商品7'},
{key: '商品8'},
{key: '商品9'},
{key: '商品10'},
{key: '商品11'},
{key: '商品12'},
{key: '商品13'},
{key: '商品14'},
{key: '商品15'},
{key: '商品16'},
{key: '商品17'},
{key: '商品18'}
]}
renderItem={({item}) => <Text style={styles.product_item}>{item.key}</Text>}/>
</View>
);
}
复制代码
效果如下:
这样,整个首页基本完成了,当然,需要细节优化的还有很多。
2.4 交互
按钮点击
为button增加点击事件,button中提供onPress方法,可以直接使用
<Button
style={styles.button}
title="搜索"
onPress={() => {
Alert.alert('点击搜索按钮', null, null);
}}/>
复制代码
banner点击
可以通过为Image包一层TouchableHighlight来实现,具体如下:
<TouchableHighlight
onPress={() => {
Alert.alert('点击banner', null, null);
}}>
<Image
style={{width:Dimensions.get('window').width, height: 180}}
source={image_list.img1}/>
</TouchableHighlight>
复制代码
状态栏样式修改
class StatusBarComponent extends React.Component {
render() {
return (
<View>
<StatusBar
backgroundColor={'blue'}
barStyle={'default'}
networkActivityIndicatorVisible={true}
></StatusBar>
</View>
);
}
}
复制代码
整体效果
3.总结
App首页基本完成,我们用到了flexbox用于布局,用到了声明周期方法、定时器、hook的简单使用,还用到了基本的点击交互,遇到了问题并解决了问题,后续我们会学习更多技巧,进一步完善这个应用。