停了一段时间后RN的学习又持续了💪。在停止的这段时间中主要是去做了关于iOS的一些细节加强,这部分内容也是自己之前一直想去总结出来容易遗忘和出错的地方。

随后跟进的RN学习主要是学习ScrollView相关使用手撸轮播图ListView的使用tabBar的使用Navigator的使用网络请求的简单认识,也快过年啦,先在此祝各位看友过年好~,因为回到老家网实在是不好,很多事情不方便去做😔,一些零零散散的琐事也时不时的打扰着自己,只能每天推进一点点。

在这段学习RN的时间中,我主要认识到了RN“真的很垃圾”🙂(注意:这是个双性词!),做一部分内容比Native真的方便超多的(甚至都觉得有些傻),至于哪些地方方便,之前文章中也说的七七八八了,但是重点在于做一些比如轮播图、TabBar这些Native支持得非常好的组件,在RN中就尴尬得不行(后文细谈)。所以在这段时间的学习中我差点萌生了“劝退自己”的想法,因为实在是太恶心了,先不管RN真正的精髓与我目前所理解的到底对不对得上,单是这几个基本组件在RN中实现起来都如此“恶心”(用官方的话来说,底层就是这么做的🙄),说句良心话,我实在是受不了。而且RN目前来说,虽然说是“Learn once,run anywhere”(有可能记错了?),但是iOS和Android现在位于RN生态中的分割还是太大,也有可能是自己的功夫不到家,单是一个tabBarIOS我就受不了(当然,也有可能是因为RN还需要再发展?)

我们先来看看使用ScrollView实现的轮播图效果

看上去效果还行,能够实现触摸拖动中断,但是因为整体没搭建完,并未能实现滑动整个View让轮播图进行中断停止(具体效果可参考京东)。整体实现思想跟iOS Native是一样的,同样都是需要ScrollView作为轮播容器,使用Image标签作为轮播实体,其与原生ScrollView使用起来无二意,就是基于iOS的runtime的封装

  • 需要设置高度

    • 直接设置高度(不推荐)
    • 在CSS中不加flex:1。让ScrollView中的内容自动的填充扩张
  • ScrollView内部的其他响应者尚无法阻止ScrollView本身成为响应者,ScrollView在其所有子控件的最上层,所以ScrollView内部的其它响应者并不能阻止ScrollView本身成为响应者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<ScrollView
ref={'scrollView'}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={(e) => this.onAnimationEnd(e)}
onScrollBeginDrag={(e) => this.onScrollBeginDrag(e)}
onScrollEndDrag={(e) => this.onScrollEndDrag(e)}
>
{this.renderChildView()}
</ScrollView>

renderChildView() {
var childViews = [];
var imgArr = imageData.data;
for (var i = 0; i < imgArr.length; i++) {
var imgItem = imgArr[i];
console.log(imgItem)
childViews.push(
<Image key={i} source={require('./img/lunbo1.png')} style={{width: screenWidth, height: 120}}/>
);
}
return childViews;
}

以上就是轮播图的架构(非常简单)。数据结构如下,用的是本地图片资源,(这里有个大坑,后文详解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"data" : [
{
"img" : "lunbo1",
"title" : "2333"
},
{
"img" : "lunbo2",
"title" : "2333"
},
{
"img" : "lunbo3",
"title" : "2333"
}
]
}

想要让轮播图在一定时间间隔后进行自动轮播,必不可少的需要一个定时器进行时间的计算,据部分资料显示,ES5需要一个react-timer-mixin组件,而采用ES6后可以大大减少累赘的写法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 组件都载入完毕后,调起计时器方法
componentDidMount() {
this.startTimer();
}

startTimer() {
var scrollView = this.refs.scrollView;
var imgCount = imageData.data.length;
// 有setTimeOut和setInterval,需注意分清
this.timer1 = setInterval(() => {
var activePage = 0;
// 判断当前分页
if ((this.state.currentPage + 1) >= imgCount) {
activePage = 0
} else {
activePage = this.state.currentPage + 1;
}

this.setState({
currentPage: activePage
});

// 进行偏移
var offSetX = activePage * screenWidth;
scrollView.scrollResponderScrollTo({x: offSetX , y: 0, animated: true})}, 1000);
}

// 开始拖拽
// 需要把ScrollView自身作为参数传入
onScrollBeginDrag(e) {
// ES6清除计时器
clearInterval(this.timer1);
}

// 结束拖拽
onScrollEndDrag(e) {
this.startTimer();
}

此时运行起工程,就能够发现轮播图的效果已经实现了,但是如果我们想要添加一个分页指示器那该怎么做呢?当时我的脑子就冒出了RN肯定也是封装了UIPageController,但又被事实啪啪啪的打脸,根本没有,查阅了一番资料后发现居然有网友通过了HTML中的特殊转义字符&bull;达到了效果。

这是补充完的render方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
render() {
return (
<View style={styles.container}> <ScrollView
ref={'scrollView'}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={(e) => this.onAnimationEnd(e)}
onScrollBeginDrag={(e) => this.onScrollBeginDrag(e)}
onScrollEndDrag={(e) => this.onScrollEndDrag(e)}
>
{this.renderChildView()}
</ScrollView>
{/* 返回指示器 */}
<View style={styles.pageViewStyle}>
{this.renderPageCircle()}
</View>
</View>
);
}

这是补充完的renderChildViewrenderPageCircle方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 轮播图创建方法
renderChildView() {
var childViews = [];
var imgArr = imageData.data;
for (var i = 0; i < imgArr.length; i++) {
var imgItem = imgArr[i];
console.log(imgItem)
childViews.push(
<Image key={i} source={require('./img/lunbo1.png')} style={{width: screenWidth, height: 120}}/>
);
}
return childViews;
}

// 分页指示器创建方法
renderPageCircle() {
var indicatorArr = [];
var style;
var imgArr = imageData.data;
for (var i = 0; i < imgArr.length; i++) {
style = (i === this.state.currentPage) ? {color: 'orange'} : {color: '#ffffff'};
indicatorArr.push(
<Text key={i} style={[{fontSize: 25}, style]}>&bull;</Text>
)
}
return indicatorArr;
}

以上就是使用RN实现的简单轮播图组件demo,整体来看比iOS Native实现起来更为方便快捷(重点是简明🙂,项目工程地址文后),同样也是通过获取ScrollView的偏移量进行操作,从思想上看给人感觉全都是一样的呢🙂,接着我们来看看RN中的ListView。

在学习ListView过程中实现了三个小demo,普通列表展示、吸顶列表展示(对比iOS tableView的group样式)、九宫格。其中,给我的感觉如果只是使用到ListView做一些简单的数据展示,那真的是无比的舒服,并且得益于RN自身的架构设计,我们能够很好的处理数据源的切换。

使用ListView需要这两步:

  • 创建一个ListView.DataSource数据源,给它传递一个普通的数据数组;

  • 使用数据源(data source)实例化一个ListView组件,定义一个回调函数,函数接受数组中的每个数据作为参数,并返回一个可渲染的组件(就是每一个Cell)

普通列表的实现方法在吸顶列表中有重复,在此我们只对吸顶列列表做讲解(在讲解中大家将会看见是有多么的恶心🙂)

首先我们需要给ListView设置数据源,而ListView的数据源设置带了我一个无比的震撼(太尼玛麻烦了🙂)

1
2
3
4
5
6
7
8
constructor(props) {
super(props);
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
};
}

通过以上代码我们就定义好了关于ListView的数据加载方法,当r1行不等于r2行时,才进行ListView的数据加载。我们先不管中间数据源如何更替,我们先来看如何填充ListView的核心数据源,

1
2
3
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs)
});

通过以上方法,我们会调起RN的setState异步赋值方法,其实我觉得RN从架构上就是MVVM(不知道猜的对不对),当我们在setState方法中更改了相关的属性值,使用到这些相关属性值的对应组件会自动异步的刷新UI,这点超强大的好不好🙂。在iOS中单是要实现MVVM架构思想,就得拉出KVO这种看上去非常难受的东西🙂。

在ListView中要实现黏性头部,需要使用 cloneWithRowsAndSections方法,将 dataBlob(object), sectionIDs (array), rowIDs (array) 三个值传进去。

如果你曾经有过iOS开发经验(没有也行),要实现的吸顶效果,实际上就是对ListView做了分组,取名为Section,组内的每一行称之为Row,因此我们实现section和row的数据源赋值,这些东西都需要在页面初始化方法中得到解决,因此补充完的constructor方法为,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
constructor(props){
super(props);
// 初始化section方法
var getSectionData = (dataBlob, sectionID) => {
return dataBlob[sectionID];
}

// 初始化row方法
var getRowData = (dataBlob, sectionID, rowID) => {
return dataBlob[sectionID + ":" + rowID];
}

this.state = {
dataSource: new ListView.DataSource({
// 绑定section数据刷新
getSectionData: getSectionData,
// 绑定row数据刷新,只更新渲染数据变化的那一行 ,rowHasChanged方法会告诉ListView组件是否需要重新渲染当前那一行。
getRowData: getRowData,
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
})
};
};

我们需要搭建的界面代码如下所示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
render() {
return (
<View style={styles.containerStyle}>
// 先假装有一个NavigationBar🙂
<View style={styles.headerViewStyle}>
<Text style={{color: "white", fontSize: 25}}>PJHubs</Text>
</View>
<ListView
dataSource={this.state.dataSource}
// ListViewRow的数据绑定
renderRow={this.renderRow}
// ListViewSection的数据绑定
renderSectionHeader={this.renderSectionHeader}
// 设置Cell内部样式
contentContainerStyle={styles.listViewStyle}
/>
</View>
)
}

// 创建ListViewRow
renderRow(rowData) {
return (
<TouchableOpacity activeOpacity={0.5}>
<View style={styles.innerViewStyle}>
<Image source={require("./wine.png")} style={styles.leftImageStyle} />
<Text style={{fontSize: 17}}>{rowData.name}</Text>
</View>
</TouchableOpacity>
);
}
// 创建ListViewSection
renderSectionHeader(sectionData, sectionID) {
return(
<View style={styles.sectionViewStyle}>
<Text style={{marginLeft: 10,}}>{sectionData}</Text>
</View>
)
}

因为ListView的数据加载属于耗时操作(不管数据在本地还是网络上),RN官方推荐所有的耗时操作统统给我放到componentDidMount方法中(组件加载完毕后),因此补充完的componentDidMount方法如下所示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
componentDidMount() {
this.loadDataFromJson()
}

loadDataFromJson() {
var jsonData = wine.data;
var dataBlob = {},
sectionIDs = [],
rowIDs = [],
cars = [];

for (var i = 0; i < jsonData.length; i++) {
sectionIDs.push(i);
dataBlob[i] = jsonData[i].title;
cars = jsonData[i].cars;
rowIDs[i] = [];

for (var j = 0; j < cars.length; j ++) {
rowIDs[i].push(j);
// 这是最恶心的地方,我们需要按照固定格式去填充输数据见后文,
dataBlob[i + ":" + j] = cars[j];
}
}

// 随后数据更新即可异步刷新UI
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs)
});
}

之所以说恶心,主要是因为我们要实现ListView的数据刷新,需要格式化数据格式按照sectionID:rowID的方式才能被ListView的黏性属性特性所识别,数据是下属一个sectionID才会被归纳为一个组中,具体数据样式如下所示,

1
2
3
4
5
6
7
{
"sectionID1" : "2333",
"sectionID1:rowID1" : "2333",
"sectionID1:rowID2" : "2333",
"sectionID2" : "2333",
"sectionID2:rowID1" : "233"
}

一些其它的小细节和CSS就不放出来了,项目细节见文末,po一张吸顶完成图

在RN中实现九宫格,你可以认为是iOS中collectionView,主要还是用到了ListView组件,只不过修改的内容在ListView的CSS中,核心ListView CSS如下所示,

1
2
3
4
listViewStyle: {
flexDirection: 'row',
flexWrap: 'wrap'
},

剩下的实现方法与在iOS中手撸九宫格效果是一样的,比如根据当前屏幕宽度算每个Cell的左右边距,设置个数等等。

tabBar是我最喜欢RN的地方(实现是太方便了🙂),因为tabBar在RN中有平台差异性,在此我们先用RN本身提供的tabBarIOS进行实现,先来看看到底是怎么写的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
render() {
return (
<View style={styles.container}>
<View style={styles.headViewStyle}>
<Text style={{fontSize: 25, marginTop: 10}}>PJHubs</Text>
</View>
<TabBarIOS
barTintColor= "black"
style={{flex: 1}}
>
<TabBarIOS.Item
systemIcon= "contacts"
badge= "3"
selected={this.state.selectedTabBarItem == "home"}
onPress={() => {this.setState({selectedTabBarItem: "home"})}}
>
<View style={[styles.commonViewStyle, {backgroundColor: "red"}]}>
<Text style={styles.commonTextStyle}>首页</Text>
</View>
</TabBarIOS.Item>

<TabBarIOS.Item
systemIcon= "bookmarks"
selected={this.state.selectedTabBarItem == "second"}
onPress={() => {this.setState({selectedTabBarItem: "second"})}}
>
<View style={[styles.commonViewStyle, {backgroundColor: "blue"}]}>
<Text style={styles.commonTextStyle}>首页</Text>
</View>
</TabBarIOS.Item>

<TabBarIOS.Item
systemIcon= "downloads"
selected={this.state.selectedTabBarItem == "third"}
onPress={() => {this.setState({selectedTabBarItem: "third"})}}
>
<View style={[styles.commonViewStyle, {backgroundColor: "green"}]}>
<Text style={styles.commonTextStyle}>首页</Text>
</View>
</TabBarIOS.Item>

<TabBarIOS.Item
systemIcon= "search"
selected={this.state.selectedTabBarItem == "four"}
onPress={() => {this.setState({selectedTabBarItem: "four"})}}
>
<View style={[styles.commonViewStyle, {backgroundColor: "purple"}]}>
<Text style={styles.commonTextStyle}>首页</Text>
</View>
</TabBarIOS.Item>
</TabBarIOS>
</View>
);
}

从以上代码中可以看到,主要是就是tabBarIOStabBarIOS.Item这两个组件,关于每个横向的tabBar中要显示什么,直接在tabBarItem闭合标签中写明即可。需要注意的地方是,RN中tabBar比iOS原生TabBar多了一步手动选中的操作(也可以认为是更加简明🙂),我们需要先指明一个第一次进来选中的tab,

1
2
3
4
5
6
constructor(props) {
super(props);
this.state = {
selectedTabBarItem: "home"
};
}

随后,如上图tabBar创建代码所示,根据selected属性和onPress回调函数进行tab的选中判断和变量赋值。实际上Native的tabBar选中判断方法也如此这般的操作,只不过我还是喜欢Native的做法🙂。

详细代码见工程,learnRN3、learnRN_ListView