源码在此
先看一下预览图效果:
首先通过构造器初始化state
constructor(props) {
super(props);
this.state = {
//Global这里是全局变量
loadingState: Global.loading,
contactData: null,
}
}
在render中根据state中的loadingState判断页面属于那种状态。
render() {
switch (this.state.loadingState) {
case Global.loading:
this.getContacts();
return this.renderLoadingView();
case Global.loadSuccess:
return this.renderSuccessView();
case Global.loadError:
return this.renderErrorView();
default:
}
}
状态一:加载中
该状态会获取数据源并显示“正在获取联系人数据 . . . ”,以下数据源是通过fetch指令网络获取的接口拿到的数据,获取的数据也在下面json格式数据中展现。
getContacts() {
var url = "http://app.yubo725.top/friends";
fetch(url).then((res) => res.json())
.then((json) => {
// UserInfoUtil.setUserInfo(json);
//添加存在本地操作
this.setState({
loadingState: Global.loadSuccess,
contactData: json
})
})
}
顺便给出网络请求返回的json数据格式如下:
[
{
"pinyin": "heihei",
"nick": "这锅我不背",
"name": "heihei",
"avatar": "http://rnwechat.oss-cn-beijing.aliyuncs.com/c8c0fd30-32ef-490b-9d8e-553f0c46cdc3.jpg"
},
......
]
加载过程的页面渲染:
renderLoadingView() {
return (
<View style={styles.container}>
<View style={styles.sBar} backgroundColor={Global.titleBackgroundColor}/>
<TitleBar nav={this.props.navigation}/>
<View style={styles.content}>
<CommonLoadingView hintText={"正在获取联系人数据..."}/>
</View>
</View>
);
}
其中用到了两个自定义组件“TitleBar”和“CommonLoadingView”,代码也列出如下:
import React, {Component} from 'react';
import MenuPopWindow from '../common/PopupWindow';
import Global from '../utils/Global';
import {
StyleSheet,
Text,
View,
Image,
Dimensions,
TouchableOpacity,
Button,
Platform
} from 'react-native';
const {width, height} = Dimensions.get('window');
export default class TitleBar extends Component {
constructor(props) {
super(props);
this.state = {
showPop: false,
}
}
renderAndroid() {
return (
<View style={styles.titleBarContainer}>
<View style={styles.titleBarTextContainer}>
<Text style={styles.title}>人脉</Text>
</View>
<View style={styles.titleBarButtonContainer}>
<TouchableOpacity activeOpacity={0.5} onPress={this.handleSearchClick}>
<Image
source={require('../img/ic_search.png')}
style={styles.titleBarImg}
/>
</TouchableOpacity>
<TouchableOpacity activeOpacity={0.5} onPress={this.handleAddClick}>
<Image
source={require('../img/ic_add.png')}
style={styles.titleBarImg}
/>
</TouchableOpacity>
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: width,
height: height
}}>
<MenuPopWindow
width={140}
height={200}
show={this.state.showPop}
closeModal={(show) => {
this.setState({showPop: show})
}}
menuIcons={[require('../img/ic_pop_group_chat.png'), require('../img/ic_pop_add_friends.png'),
require('../img/ic_pop_scan.png'), require('../img/ic_pop_help.png')]}
menuTexts={['发起群聊', '添加朋友', '扫描名片', '帮助与反馈']}
/>
</View>
</View>
</View>
);
}
return this.renderAndroid();
}
handleSearchClick = () => {
// 跳转到SearchScreen界面
// this.props.nav.navigate('Search');
};
handleAddClick = () => {
this.setState({showPop: !this.state.showPop});
}
}
class CustomModal extends Component {
constructor(props) {
super(props);
this.state = {
modalVisible: false,
}
}
render() {
return (
<Modal
animationType={"fade"}
transparent={true}
visible={this.state.modalVisible}
onRequestClose={() => {
alert("Modal has been closed.")
}}>
<View style={modalStyle.container}>
<View style={modalStyle.content}>
<Text>Hello World! This is a Modal!</Text>
<Button
style={{marginTop: 20}}
title={"Close"}
onPress={() => {
this.setState({modalVisible: false})
}}/>
</View>
</View>
</Modal>
);
}
closeModel = () => {
this.setState({modalVisible: false});
}
openModal() {
this.setState({modalVisible: true});
}
}
const modalStyle = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)'
},
content: {
width: width - 40,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 20,
marginRight: 20,
backgroundColor: '#FFFFFF',
height: 100,
borderRadius: 5,
paddingTop: 10,
paddingBottom: 10,
paddingLeft: 10,
paddingRight: 10,
}
});
const styles = StyleSheet.create({
titleBarContainer: {
flexDirection: 'row',
width: width,
height: 50,
backgroundColor: Global.titleBackgroundColor
},
titleBarTextContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 10,
paddingRight: 10,
},
titleBarButtonContainer: {
alignItems: 'center',
flexDirection: 'row',
paddingLeft: 10,
paddingRight: 10,
},
title: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: 'bold',
},
titleBarImg: {
width: 25,
height: 25,
marginLeft: 15,
marginRight: 15,
}
});
TitleBar添加了菜单窗口组件MenuPopWindow,效果图:
import React, {Component} from 'react';
import {
StyleSheet,
Text,
View,
ActivityIndicator
} from 'react-native';
export default class CommonLoadingView extends Component {
render() {
return (
<View style={styles.container}>
<ActivityIndicator size="large"/>
<Text style={{marginTop: 15, fontSize: 16}}>{this.props.hintText || "加载中,请稍等..."}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}
});
状态二:加载完成
获取数据之后,需要展示在页面中,怎么样展示是个问题,参考微信通讯录的展示,大概模型如下:
renderSuccessView:
renderSuccessView() {
var listData = [];
var headerListData = [];
var headerImages = [require('../../images/ic_new_friends.png'), require('../../images/ic_group_chat.png'),
require('../../images/ic_tag.png'), require('../../images/ic_common.png')];
var headerTitles = ['新的朋友', '群聊', '标签', '公众号'];
var index = 0;
for (var i = 0; i < headerTitles.length; i++) {
headerListData.push({
key: index++,
title: headerTitles[i],
nick: '',
icon: headerImages[i],
sectionStart: false,
});
}
var contacts = this.state.contactData;
for (var i = 0; i < contacts.length; i++) {
// var pinyin = PinyinUtil.getFullChars(contacts[i].name);
var pinyin = contacts[i].pinyin.toUpperCase();
var firstLetter = pinyin.substring(0, 1);
if (firstLetter < 'A' || firstLetter > 'Z') {
firstLetter = '#';
}
let icon = require('../../images/avatar.png');
if (!Utils.isEmpty(contacts[i].avatar)) {
icon = {uri: contacts[i].avatar};
}
listData.push({
key: index++,
icon: icon,
title: contacts[i].name,
nick: contacts[i].nick,
pinyin: pinyin,
firstLetter: firstLetter,
sectionStart: false,
})
}
// 按拼音排序
listData.sort(function (a, b) {
if (a.pinyin === undefined || b.pinyin === undefined) {
return 1;
}
if (a.pinyin > b.pinyin) {
return 1;
}
if (a.pinyin < b.pinyin) {
return -1;
}
return 0;
});
listData = headerListData.concat(listData);
// 根据首字母分区
for (var i = 0; i < listData.length; i++) {
var obj = listData[i];
if (obj.pinyin === undefined) {
continue;
}
if (i > 0 && i < listData.length) {
var preObj = listData[i - 1];
if (preObj.pinyin === undefined && obj.pinyin !== undefined) {
obj.sectionStart = true;
} else if (preObj.pinyin !== undefined && obj.pinyin !== undefined && preObj.firstLetter !== obj.firstLetter) {
obj.sectionStart = true;
}
}
}
this.listData = listData;
return (
<View style={styles.container}>
<TitleBar nav={this.props.navigation}/>
<View style={styles.divider}/>
<View style={styles.content}>
<FlatList
ref={'list'}
data={listData}
renderItem={this._renderItem}
getItemLayout={this._getItemLayout}
/>
<SideBar onLetterSelectedListener={this.onSideBarSelected.bind(this)}/>
</View>
<View style={styles.divider}/>
</View>
);
}
其中用到的列表展示器是FlatList,在return之前还有一系列操作,这些操作是针对按字母分组的需求设计的。
Item的样式:
是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。如果你的行高是固定的,那么getItemLayout用起来就既高效又简单,类似下面这样:
getItemLayout={(data, index) => ( {length: 行高, offset: 行高 * index, index} )}
注意:如果你指定了SeparatorComponent,请把分隔线的尺寸也考虑到offset的计算之中。
_getItemLayout = (data, index) => {
let len = data.sectionStart ? (58) : (51);
let dividerHeight = 1 / PixelRatio.get();
return {
length: len,
offset: (len + dividerHeight) * index,
index
};
}
Item的内容:
根据上面的拼音分区拿到的sectionStart作为条件判断,渲染每一行的组件。
_renderItem = (item) => {
var section = [];
if (item.item.sectionStart) {
section.push(<Text key={"section" + item.item.key}
style={listItemStyle.sectionView}>{item.item.firstLetter}</Text>);
}
return (
<View>
{section}
<TouchableHighlight underlayColor={Global.touchableHighlightColor} onPress={() => {
this.onListItemClick(item)
}}>
<View style={listItemStyle.container} key={item.item.key}>
<Image style={listItemStyle.image} source={item.item.icon}/>
<Text style={listItemStyle.itemText}>{item.item.title}</Text>
<Text
style={listItemStyle.subText}>{Utils.isEmpty(item.item.nick) ? "" : "(" + item.item.nick + ")"}</Text>
</View>
</TouchableHighlight>
<View style={{
width: width,
height: 1 / PixelRatio.get(),
backgroundColor: Global.dividerColor
}}/>
</View>
);
}
侧边的SideBar组件,可以实现滑动索引的效果 > SideBar
import React, {Component} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
export default class SideBar extends Component {
render() {
var letters = ['☆', '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
var letterViewArr = [];
for (var i = 0; i < letters.length; i++) {
letterViewArr.push(
<TouchableOpacity
key={i}
onPress={this.onLetterSelectedListener.bind(this, letters[i])}>
<Text style={{color: '#999999', fontSize: 12, paddingLeft: 2, paddingRight: 2}} key={"letter" + i}>
{letters[i]}
</Text>
</TouchableOpacity>
);
}
return (
<View
style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', backgroundColor: '#FFFFFF'}}>
{letterViewArr}
</View>
);
}
onLetterSelectedListener = (letter) => {
// Toast.showShortCenter(letter);
this.props.onLetterSelectedListener && this.props.onLetterSelectedListener(letter);
}
}
该组件暴露了一个接口与我们实现 > onSideBarSelected(letter):
onSideBarSelected(letter) {
if (this.listData) {
for (let i = 0; i < this.listData.length; i++) {
let item = this.listData[i];
if (item.firstLetter == letter && item.sectionStart) {
//根据FlatList的ref属性调用其方法scrollToIndex()可以跳转到对应拼音的联系人
Toast.showShortCenter(letter);
this.refs.list.scrollToIndex({viewPosition: 0, index: i});
break;
}
}
}
}
三、加载错误
renderErrorView() {
return (
<View style={{
justifyContent: 'center',
alignItems: 'center',
flex: 1,
flexDirection: 'column'
}}>
<Text style={{fontSize: 16, color: '#000000'}}>加载数据出错!</Text>
</View>
);
}