一、微组件的封装
每个页面的导航都会有不同的样式或者图片,为了实现代码的复用性,可以将导航统一封装成一个微小组件。
封装的Item需要有可点击事件,需要显示文字和图片。
import React, { PureComponent } from 'react'
import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native'
export default class NavigationItem extends PureComponent {
render() {
// 图片和文字时从外界传入的 所以用到props
let icon = this.props.icon &&
<Image style={[styles.icon, this.props.iconStyle]} source={this.props.icon} />
let title = this.props.title &&
<Text style={[styles.title, this.props.titleStyle]}>{this.props.title}</Text>
return (
<TouchableOpacity style={styles.container} onPress={this.props.onPress}>
{icon}
{title}
</TouchableOpacity>
);
}
}
// 样式
const styles = StyleSheet.create({
container: {
flex:1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
icon: {
width: 27,
height: 27,
margin: 8,
},
title: {
fontSize: 15,
color: '#333333',
margin: 8,
}
});
再来分析每个页面都有不同的字体出现,比如说标题、段落、小标题。所以也可以将文字封装成单独的组件。
import React from 'react';
import ReactNative, { StyleSheet, Dimensions, Text ,ReactElement} from 'react-native'
import color from './color'
export function Heading1({style, ...props}) {
return <Text style={[styles.h1, style]} {...props} />
}
export function Heading2({style, ...props}) {
return <Text style={[styles.h2, style]} {...props} />
}
export function Paragraph({style, ...props}) {
return <Text style={[styles.p, style]} {...props} />
}
//设置样式
const styles = StyleSheet.create({
h1: {
fontSize: 15,
fontWeight: 'bold',
color: '#222222',
},
h2: {
fontSize: 14,
color: '#222222',
},
p: {
fontSize: 13,
color: '#777777',
},
});
将整个页面分为两部分,页面最下方是一个列表,可以当成是iOS中的tableView,而页面上方可以看做是头部的View,这个View里面存放了各种模块。模块之间会有分割线,可以将分割线也封装成组件。
import React, { PureComponent } from 'react'
import { View, Text, StyleSheet } from 'react-native'
import color from './color'
export default class SpacingView extends PureComponent {
render() {
return (
<View style={styles.container}>
</View>
);
}
}
// define your styles
const styles = StyleSheet.create({
container: {
height: 14,
backgroundColor: color.background,
},
});
在设置样式的时候,会用到全屏,因此可以把全屏幕的尺寸也封装起来。
import { Dimensions, Platform, PixelRatio } from 'react-native'
export default {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
onePixel: 1 / PixelRatio.get(), //在iphone4+中代表一个像素点
statusBarHeight: (Platform.OS === 'ios' ? 20 : 0) //iOS平台状态栏默认为0,安卓平台默认为20
}
二、首页封装
1) 首页导航的实现
同样也是在navigationOptions调用箭头函数。返回标题、图片。当然左右两边可以返回已经封装好的NavigationItem属性。
static navigationOptions = ({ navigation }) => ({
headerTitle: (
<TouchableOpacity style={styles.searchBar}>
<Image source={require('../../img/Home/search_icon.png')} style={styles.searchIcon} />
<Paragraph>一点点</Paragraph>
</TouchableOpacity>
),
headerRight: (
<NavigationItem
icon={require('../../img/Home/icon_navigationItem_message_white.png')}
onPress={() => {
}}
/>
),
headerLeft: (
<NavigationItem
title='福州'
titleStyle={{ color: 'white' }}
onPress={() => {
}}
/>
),
headerStyle: { backgroundColor: color.theme },
})
2)列表的实现
首页要实现列表功能,采用组件FlatList。
- 引用必要的组件
import color from '../../widget/color'
import NavigationItem from '../../widget/NavigationItem'
import SpacingView from '../../widget/SpacingView'
import screen from '../../common/screen'
import api from '../../api'
import { Heading1, Heading2, Paragraph } from '../../widget/Text'
- 在render方法中返回FlatList并设置它的属性
render() {
return (
<View style={styles.container}>
<FlatList
data={this.state.dataList} //请求的数据
keyExtractor={this.keyExtractor} //设置每个item唯一的key
onRefresh={this.requestData} //刷新的操作
refreshing={this.state.refreshing} //刷新状态
ListHeaderComponent={this.renderHeader} //头部页面展示
renderItem={this.renderCell} //每个item
/>
</View>
);
}
- 存放的数据要在state中声明并指定属性。(类中)
state: {
discounts: Array<Object>,
dataList: Array<Object>,
refreshing: boolean,
}
- 在构造函数中设置初始值,并绑定要实现的方法
constructor(props) {
super(props)
this.state = {
discounts: [],
dataList: [],
refreshing: false,
}
{ this.requestData = this.requestData.bind(this) }
{ this.renderCell = this.renderCell.bind(this) }
{ this.onCellSelected = this.onCellSelected.bind(this) }
{ this.keyExtractor = this.keyExtractor.bind(this) }
{ this.renderHeader = this.renderHeader.bind(this) }
{ this.onGridSelected = this.onGridSelected.bind(this) }
{ this.onMenuSelected = this.onMenuSelected.bind(this) }
}
- 实现方法
- keyExtractor设置每个item唯一的key
keyExtractor(item, index) {
return item.id
}
- onRefresh对应的requestData方法做刷新的操作
每次请求数据,都要进行刷新,所以刷新的状态要改为true。而且要刷新之后,要刷新折扣版块和列表版块的内容。为了代码简洁,将这两个功能封装成方法
requestData() {
//每次请求数据 都要进行刷新
this.setState({ refreshing: true })
//调用折扣
this.requestDiscount()
//调用推荐方法
this.requestRecommend()
}
- 实现折扣模块的方法
这里使用ES7的的异步线程语法。await表示紧跟在后面的表达式需要等待结果。也就是说当执行到awiat的时候,执行器将交给其他线程,等执行权返回再从暂停的地方往后执行。
这里做的是请求数据的操作,用fetch函数传入api得到全部的折扣数据结果。然后再转化为json数据,接着把json中的data赋值给discounts数组。
async requestDiscount() {
try {
let response = await fetch(api.discount)
let json = await response.json()
this.setState({ discounts: json.data })
} catch (error) {
alert('错误信息:'+error)
}
}
-
实现推荐列表的方法
上图是从API中的recommend的url解析出来的json数据。选取需要的数据。在代码中用fetch将数据解析成json格式,取出data集合中的数据传入箭头函数中,一一赋值给指定变量之后返回给数组dataList。如此一来,就可以用setState方法改变数组的数据。
async requestRecommend(){
try{
let response = await fetch(api.recommend)
let json = await response.json()
let dataList = json.data.map(
(info)=>{
return{
id:info.id,
imageUrl:info.squareimgurl,
title:info.mname,
subtitle:`$[{info.range}]${info.title}`,
price:info.price
}
}
)
this.setState({
dataList:dataList,
refreshing:false,
})
}catch (error){
this.setState({
refreshing:false
})
}
}
为了不影响渲染效果,可以将请求数据的方法放在componentDidMount
方法中
componentDidMount() {
this.requestData()
}
- 处理列表
在iOS或者其他编程语言中,会采用各类框架来防止代码冗余。最常用的是MVC模式。在本项目中,为了返回列表,可以先将列表的UI封装起来。
import React,{Component} from 'react'
import {View,Text,StyleSheet,TouchableOpacity,Image} from 'react-native'
import {Heading1,Paragraph}from '../../widget/Text'
import screen from '../../common/screen'
import color from '../../widget/color'
export default class GroupPurchaseCell extends Component{
render(){
let {info} = this.props
let imageUrl = info.imageUrl.replace('w.h','160.0')
return(
<TouchableOpacity style={style.container} onPress = {()=>this.props.onPress(info)}>
<Image source={{uri:imageUrl}} style={styles.icon}/>
<View style={styles.rightContainer}>
<Heading1>{info.title}</Heading1>
<View>
</View>
<Paragraph numberOfLines={0} style={{ marginTop: 8 }}>{info.subtitle}</Paragraph>
<View style={{ flex: 1, justifyContent: 'flex-end' }}>
<Heading1 style={styles.price}>{info.price}元</Heading1>
</View>
</View>
</TouchableOpacity>
)
}
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
padding: 10,
borderBottomWidth: screen.onePixel,
borderColor: color.border,
backgroundColor: 'white',
},
icon: {
width: 80,
height: 80,
borderRadius: 5,
},
rightContainer: {
flex: 1,
paddingLeft: 20,
paddingRight: 10,
},
price: {
color: color.theme
}
});
- 到首页中调用
引入框架
import GroupPurchaseCell from '../GroupPurchase/GroupPurchaseCell'
将数据传给cell并调用
renderCell(info) {
return (
<GroupPurchaseCell
info={info.item}
onPress={this.onCellSelected}
/>
)
}
实现cell的onPress方法
onCellSelected(info) {
//把状态栏的样式给成暗色
StatusBar.setBarStyle('default', false)
//跳转到详情页面,并把本页信息传递给详情页
this.props.navigation.navigate('GroupPurchase', { info: info })
}
当点击cell的时候,导航会跳转到详情页面,那么就要把要跳转页面的名称传入到navigate中。
- 为了测试,先简单封装详情页
建立一个.js文件GroupPurchaseScene,用来测试
import React, { Component } from 'react'
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ListView, Image, InteractionManager } from 'react-native'
export default class GroupPurchaseScene extends Component {
render(){
return(
<View>
<Text>
详情页面
</Text>>
</View>
)
}
}
const Styles = StyleSheet.create({
container: {
flex:1,
backgroundColor:'pink'
}
})
- 测试
在RootScene中引用详情页,并把它加入到导航当中
import GroupPurchaseScene from './scene/GroupPurchase/GroupPurchaseScene'
const Navigator = StackNavigator(
{
Tab: { screen: Tab }, //框架的页面
// Web: { screen: WebScene }, //webview的页面
GroupPurchase: { screen: GroupPurchaseScene },//详情页
},
为了测试,先把首页的renderHeader()方法添加进去
renderHeader(){
return(
<View>
<Text style={{fontSize:24}}>头部啊头部ddddddddddnihaoaha</Text>
</View>
)
}
测试结果如下:
点击详情页也能跳转
- 首页导航实现
static navigationOptions = ({ navigation }) => ({
headerTitle: (
<TouchableOpacity style={styles.searchBar}>
<Image source={require('../../img/Home/search_icon.png')} style={styles.searchIcon} />
<Paragraph>一点点</Paragraph>
</TouchableOpacity>
),
headerRight: (
<NavigationItem
icon={require('../../img/Home/icon_navigationItem_message_white.png')}
onPress={() => {
}}
/>
),
headerLeft: (
<NavigationItem
title='福州'
titleStyle={{ color: 'white' }}
onPress={() => {
}}
/>
),
headerStyle: { backgroundColor: color.theme },
})
- 添加样式
searchBar: {
width: screen.width * 0.7,
height: 30,
borderRadius: 19,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
alignSelf: 'center',
},
searchIcon: {
width: 20,
height: 20,
margin: 5,
}
- 封装头部
头部分为2个部分,一个是滚动的部分,另一个则是格子的部分。并且这两部分底部都有一个分割线。现在来封装这两个部分。
先封装最上方的部分,新建一个HomeMenuView.js。而这个部分又是由许多小的view组成的。所以将这些小的view也封装起来。新建一个文件HomeMenuItem。而这个类返回的只要是一个图片和文字即可。
import React, {Component } from 'react'
import { View, Text, StyleSheet, Image, TouchableOpacity } from 'react-native'
import { Heading2 } from '../../widget/Text'
import screen from '../../common/screen'
export default class HomeMenuItem extends Component{
render(){
return(
<TouchableOpacity
style={styles.container}
onPress = {this.props.onPress}
>
<Image source={this.props.icon} resizeMode='contain' style={styles.icon}/>
<Heading2>
{this.props.title}
</Heading2>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
width: screen.width / 5,
height: screen.width / 5,
},
icon: {
width: screen.width / 9,
height: screen.width / 9,
margin: 5,
}
});
现在来封装HomeMenuView
这个View的主体是一个ScrollView和一个PageControl,当然RN中是没有特定的页面控制器的,所以该PageControl是需要自己封装的。把它放入widget组件中。
现在考虑PageControl需要什么属性。属性可以用propTypes来规定,然而最近React组件已经把PropTypes组件移除了React库,所以需要引入prop-types。import PropTypes from 'prop-types';
numberofPage:首先是各个组件能够分为多少页,这个是必须要传入的。规定为number类型。可以用isRequired来约束。
currentPage:当前页用来控制亮点会出现在哪一页,number类型。
hidesForSinglePage:当页面为1时,是否隐藏控制器,bool类型。
pageIndicatorTintColor:控制器上点的颜色,string类型。
currentPageIndicatorTintColor:当前亮点的颜色,string类型。
indicatorSize:指示器的大小,规定为object类型,
indicatorStyle:指示器的样式,它是View规定的类型,所以规定为View.propTypes.style类型
currentIndicatorStyle:当前指示器的样式。同上
onPageIndicatorPress:点击指示器的处理时间,func类型。
引入头文件
import React, {Component } from 'react'
import { View, StyleSheet, TouchableWithoutFeedback } from 'react-native'
import assign from 'object-assign';
import PropTypes from 'prop-types';
在类中规定属性类型
static propTypes = {
numberOfPages: PropTypes.number.isRequired,
currentPage: PropTypes.number,
hidesForSinglePage: PropTypes.bool,
pageIndicatorTintColor: PropTypes.string,
currentPageIndicatorTintColor: PropTypes.string,
indicatorSize: PropTypes.object,
indicatorStyle: View.propTypes.style,
currentIndicatorStyle: View.propTypes.style,
onPageIndicatorPress: PropTypes.func
}
给属性设置为默认值
static defaultProps = {
numberOfPages: 0,
currentPage: 0,
hidesForSinglePage: false,
pageIndicatorTintColor: 'gray',
currentPageIndicatorTintColor: 'white',
indicatorSize: { width: 8, height: 8 },
indicatorStyle: {},
currentIndicatorStyle: {},
onPageIndicatorPress: function () { }
}
实现方法
onPageIndicatorPress(index) {
this.props.onPageIndicatorPress(index);
}
实现render方法
render() {
//解构赋值,取出所有的属性
var { style, ...props } = this.props;
//给指示器设置默认的属性,备用
var defaultStyle = {
height: this.props.indicatorSize.height
};
//设置每个小点的样式
var indicatorItemStyle = {
width: this.props.indicatorSize.width,
height: this.props.indicatorSize.height,
borderRadius: this.props.indicatorSize.height / 2,
marginLeft: 5,
marginRight: 5
};
//指示器整体样式
var indicatorStyle = assign({}, indicatorItemStyle, this.props.indicatorStyle, {
backgroundColor: this.props.pageIndicatorTintColor
});
//选中指示器的样式
var currentIndicatorStyle = assign({}, indicatorItemStyle, this.props.currentIndicatorStyle, {
backgroundColor: this.props.currentPageIndicatorTintColor
});
//创建一个数组,将小点添加到pages里面。
var pages = [];
for (var i = 0; i < this.props.numberOfPages; i++) {
pages.push(i);
}
//页面隐藏设置为真并且页面长度<=1时,返回null,否则返回一个个的小点。
return (
this.props.hidesForSinglePage && pages.length <= 1 ? null : <View style={[styles.container, defaultStyle, style]}>
{/*从小点的集合中取出每个点,如果是当前的点就返回当前点样式,否则返回普通样式*/}
{pages.map((el, i) => <TouchableWithoutFeedback key={i} onPress={this.onPageIndicatorPress.bind(this, i)}>
<View style={i == this.props.currentPage ? currentIndicatorStyle : indicatorStyle} />
</TouchableWithoutFeedback>
)}
</View>
)
}
现在可以做封装HomeMenuView的操作了
先把render函数要返回的东西梳理清楚
return (
//scrollView里面要包含多个Item。将数组menuView添加到view里面
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.contentContainer}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
onScroll={(e) => this.onScroll(e)}
>
<View style={styles.menuContainer}>
{menuViews}
</View>
</ScrollView>
<PageControl
style={styles.pageControl}
numberOfPages={pageCount}
currentPage={this.state.currentPage}
hidesForSinglePage
pageIndicatorTintColor='gray'
currentPageIndicatorTintColor={color.theme}
indicatorSize={{ width: 8, height: 8 }}
/>
</View>
);
scrollview中包含的是menuView,也就是每个设置好样式的items。
在render方法的return方法之前,创建一个数组,该数组用来存放每个item。
render() {
//取出属性中的menuinfos和onMenuSelected
let { menuInfos, onMenuSelected } = this.props
// 将属性传给每个HomeMenuitem
let menuItems = menuInfos.map(
(info, i) => (
<HomeMenuItem
key={info.title}
title={info.title}
icon={info.icon}
onPress={() => {
onMenuSelected && onMenuSelected(i)
}} />
)
)
//创建一个menuViews数组,用来表示每一页
let menuViews = []
//像上取整得到页数
let pageCount = Math.ceil(menuItems.length / 10)
for (let i = 0; i < pageCount; i++) {
//slice() 方法可从已有的数组中返回选定的元素。start和end,表示一页放十个
let items = menuItems.slice(i * 10, i * 10 + 10)
// 一整页的
let menuView = (
<View style={styles.itemsView} key={i}>
{items}
</View>
)
//加入到所有页面的数组
menuViews.push(menuView)
}
//此处是省略的return 在上面已经写了
}
上面return方法中要返回的PageControl要将currentPage传入进去,所以在MenuView应该先有一个状态机以便在用户滚动页面的时候实施修改。
state: {
currentPage: number
}
constructor(props) {
super(props)
this.state = {
currentPage: 0
}
}
实现滚动的方法
onScroll(e) {
//拿到x的偏移量
let x = e.nativeEvent.contentOffset.x
//用偏移量/宽度得到当前页数
let currentPage = Math.round(x / screen.width)
console.log('onScroll ' + e.nativeEvent.contentOffset.x + ' page ' + currentPage + ' current ' + this.state.currentPage)
if (this.state.currentPage != currentPage) {
//改变状态机
this.setState({
currentPage: currentPage
})
}
}
设置样式
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
},
contentContainer: {
},
menuContainer: {
flexDirection: 'row',
},
itemsView: {
flexDirection: 'row',
flexWrap: 'wrap',
width: screen.width,
},
pageControl: {
margin: 10,
}
});
- 到首页中修改readerHeader方法
引入头文件
import HomeMenuView from './HomeMenuView'
将HomeMenuView添加到方法中去
renderHeader() {
return (
<View>
<HomeMenuView menuInfos={api.menuInfo} onMenuSelected={this.onMenuSelected} />
<SpacingView />
<View style={styles.recommendHeader}>
<Heading2>猜你喜欢</Heading2>
</View>
</View>
)
}
设置样式
recommendHeader: {
height: 35,
justifyContent: 'center',
borderWidth: screen.onePixel,
borderColor: color.border,
paddingVertical: 8,
paddingLeft: 20,
backgroundColor: 'white'
},
接下来封装头部的第二部分
原理同第一部分类似,建立第二个部分的js文件HomeGridView和第二部分的每一小项HomeGridItem。
HomeGridItem:
export default class HomeGridItem extends PureComponent {
render() {
let info = this.props.info
let title = info.maintitle
let color = info.typeface_color
let subtitle = info.deputytitle
let imageUrl = info.imageurl.replace('w.h', '120.0')
return (
<TouchableOpacity style={styles.container} onPress={this.props.onPress}>
<View>
<Heading1 style={{ color: color, marginBottom: 10 }}>{title}</Heading1>
<Heading2 >{subtitle}</Heading2>
</View>
<Image style={styles.icon} source={{ uri: imageUrl }} />
</TouchableOpacity>
);
}
}
// define your styles
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
width: screen.width / 2 - screen.onePixel, //一行放两个,减去一个像素点
height: screen.width / 4,
backgroundColor: 'white',
borderBottomWidth: screen.onePixel,
borderRightWidth: screen.onePixel,
borderColor: color.border
},
icon: {
width: screen.width / 5,
height: screen.width / 5,
}
});
HomeGridView:
import React, { PureComponent } from 'react';
import { View, Text, StyleSheet } from 'react-native'
import color from '../../widget/color'
import screen from '../../common/screen'
import HomeGridItem from './HomeGridItem'
export default class HomeGridView extends PureComponent {
//设定一个数组,用来接收数据
static defaultProps = {
infos: []
}
render() {
return (
<View style={styles.container}>
//将infos赋值给Item
{this.props.infos.map((info, index) => (
<HomeGridItem
info={info}
key={index}
onPress={() => this.props.onGridSelected(index)} />
))}
</View>
);
}
}
// define your styles
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
borderTopWidth: screen.onePixel,
borderLeftWidth: screen.onePixel,
borderColor: color.border
},
});
在HomeScene中
引入头文件
import HomeGridView from './HomeGridView'
修改renderHeader方法,将gridView加入进去
<HomeGridView infos={this.state.discounts} onGridSelected={(this.onGridSelected)} />
<SpacingView />
到构造函数中绑定onGridSelected和onMenuSelected
{ this.onGridSelected = this.onGridSelected.bind(this) }
{ this.onMenuSelected = this.onMenuSelected.bind(this) }
将这两个方法的原型写出来
onGridSelected(index) {
}
onMenuSelected(index) {
alert(index)
}
首页第一个界面完成,其余的页面放在之后讲解。