RN仿微信通讯录列表

源码在此


先看一下预览图效果:

pic1.jpg

首先通过构造器初始化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,效果图:

TitleBar
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'
  }
});

CommonLoadingView
状态二:加载完成

获取数据之后,需要展示在页面中,怎么样展示是个问题,参考微信通讯录的展示,大概模型如下:


pic3

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;
                }
            }
        }
    }
pic3
三、加载错误
    renderErrorView() {
        return (
            <View style={{
                justifyContent: 'center',
                alignItems: 'center',
                flex: 1,
                flexDirection: 'column'
            }}>
                <Text style={{fontSize: 16, color: '#000000'}}>加载数据出错!</Text>
            </View>
        );
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,461评论 2 59
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 2018.6.3 夏至未至 时间是让人猝不及防的东西,本以为还可以和你们在一起再待很长很长时间,没有想到,转...
    RebecaZhan_e50e阅读 313评论 0 0
  • 生活越来越匆忙,不喜欢咖啡,昏昏沉沉
    蜻停阅读 106评论 0 0
  • 新年快乐,狗年蓝图会到底!加油!梦想一定能实现!
    我就是可能阅读 98评论 0 0