react-native使用SectionList做通讯录列表,分组跳转

版权声明:原创文章,未经允许不得转载!!! https://blog.csdn.net/halo1416/article/details/82148873

前言:最近开发的app中有一个类似手机通讯录的组件,准备自己用 SectionList 做;

注意:本文使用的RN版本为0.55版本,从官方文档上看SectionList是从0.43版本才有的。如果需要使用SectionList,请注意RN的版本问题。

首先,看下最后的效果图:

其次,接口请求的数据结构(mock模拟):

然后,看一下官方对SectionList数据格式的要求:https://reactnative.cn/docs/0.55/sectionlist/

接下来,进入正题:

1. 定义三个state,存储数据

扫描二维码关注公众号,回复: 3134216 查看本文章

其实两个state就可以,我这里listData都没有用,只有把源数据保存下来了!

2. 请求数据,并将源数据拆分为SectionList需要的数据格式,并构造一个字母索引的数组

componentDidMount(){
        this.getContacts();
    }

    async getContacts() {
        try{
            let data = await API.app.contactlist();     //获取联系人列表
            const {list} = data;

            let sections = [], letterArr = [];
            list.map((item,index) => {
                if(letterArr.indexOf(item.spell.charAt(0)) === -1){
                    letterArr.push(item.spell.charAt(0) );          //得到所有不重复的首字母
                }
            });
            letterArr.sort();

            letterArr.map((item,index) => {
                const module = list.filter((it) => {         //遍历获取每一个首字母对应联系人
                        return it.spell.charAt(0) === item;
                    }
                );
                sections.push({key: item, title: item, data: module,});     //首字母对应联系人数组放到sections(每一次循环放一次)
            });

            this.setState({
                listData : list,
                letterArr,
                sections,
            });
        }catch (err) {
            console.log('err',err);
            showToast(err.message);
        }
    }

3. 构造页面(左侧为SectionList列表,右侧为字母索引列表)

const {listData, letterArr, sections} = this.state;
//偏移量 = (设备高度 - 字母索引高度 - 底部导航栏 - 顶部标题栏 - 24)/ 2         ===>> 最后这个24,我有点找不到原因,个人觉得可能是系统顶部的状态栏
const top_offset = (Dimensions.get('window').height - letterArr.length*20 - 56 - 44 - 24) / 2;
return(
    <Content contentContainerStyle={{flex:1,flexDirection:'row',justifyContent:'flex-start'}}>
        <SectionList
             ref="_sectionList"
             renderSectionHeader={this._renderSectionHeader}
             ItemSeparatorComponent={() => <View style={{height: 1, backgroundColor: '#E3E3E3',marginHorizontal:10}}/>}
             sections={sections}
             keyExtractor={(item, index) => index}
             numColumns={1}
             renderItem={({item, index}) => this._renderItem(item, index)}
        />
        <View style={{position:'absolute',width:26,right:0,top:top_offset}}>
            <FlatList
                  data= {letterArr}
                  keyExtractor = {(item, index) => index.toString()}       //不重复的key
                  renderItem={({item,index}) =>
                      <TouchableOpacity style={{marginVertical:2,height:16,flexDirection:'row',justifyContent:'center',alignItems:'center'}}>
                         <Text style={{fontSize:config.FONT_SIZE_CONTENT_TEXT}}>{item.toUpperCase()}</Text>
                      </TouchableOpacity>
                  }
            />
     </View>
</Content>
);

注意:我这里为了把右侧的字母索引列表悬浮在SectionList只是,用了定位,我觉得不是很好,因为top的偏移量需要计算,但flex我没有找到很好的出来方法。

4. 点击右侧字母导航,SectionList滚动到对应的节点上

       4.1. 给字母索引列表的item加一个点击事件

        

        4.2. 定义_onSectionselect(滚动事件)

_onSectionselect = (key) => {
     let offset = key * 20;      //点击了那个字母索引,没有section的header的高是20;key为0是即偏移0
     const {sections} = this.state;
     sections.map((item,index) => {
         if(key > index){        //要滚动的距离就是,点击的字母索引之前的所有内容,所以当 点击字母的索引 大于 sections(变量)的索引时;技术要滚动的高度
             offset = offset + item.data.length*80 + (item.data.length-1);      //每个联系人的item高是60,上下padding各为10;然后每个节点里面有length-1条分割线且高度为1
          }
     });

     //滚动到指定的偏移的位置
     this.refs._sectionList.scrollToOffset({animated: true, offset: offset});
};

这里主要计算滚动的偏移量,比如我点击了第一个字母,就需要偏移一个字母的所有数据(item)、节点头部(renderSectionHeader)和分割线的总高度,移除类推。

5. 修改SectionList源码,添加scrollToOffset方法

注意:SectionList这个列表组件并没有scrollToOffset方法(直接使用会报错:找不到scrollToOffset方法);FlatList中才有,包括scrollToIndex和scrollToEnd也是一样

SectionList的方法有:

FlatList的方法有:

所以,需要参照FlatList里面的scrollToOffset方法,给SectionList这个列表组件手动添加scrollToOffset方法

第一步:其中SectionList的路径为:node_modules/react-native/Libraries/Lists/SectionList.js,代码格式化后大概在343行的位置,修改如下:

  //添加scrollToOffset方法
  scrollToOffset(params: {animated?: ?boolean, offset: number}) {
      if (this._wrapperListRef) {
          this._wrapperListRef.scrollToOffset(params);
      }
  }

即:

第二步:同时还需要修改VirtualizedSectionList的代码,路径在node_modules/react-native/Libraries/Lists/VirtualizedSectionList.js,大概381行处修改如下:

//添加scrollToOffset方法
  scrollToOffset(params: {animated?: ?boolean, offset: number}) {
      if (this._listRef) {
          this._listRef.scrollToOffset(params);
      }
  }

即:

注意,那个 ref 变量的名称在不同的RN版本中可能不一样,请参照其余地方使用即可!

修改完毕!

完整代码:(有其他组件的引入,可能用不起,请参考)

import React, {Component} from "react";
import {
    View,
    Text,
    // Button,
    Dimensions,
    StyleSheet,
    SectionList,
    Linking,
    Platform,
    FlatList,
    Image,
    TouchableOpacity,
    ScrollView,
    TouchableWithoutFeedback,
} from 'react-native'

import {
    Container,
    Header,
    Title,
    Content,
    Button,
    Footer,
    FooterTab,
    Left,
    Right,
    Body,
    Icon,
    Form,
    Item,
    Input,
    Label,
    H1,
    ListItem,
    // Text,
    CheckBox
} from 'native-base';

import {HeaderDark, HeaderLeft, HeaderBody, HeaderRight, HeaderBack, HeaderCaption} from '../common/ui/header';
import API from "../API/API";
import '../API/API+App';
import {makeImgUrl, showToast} from "../common/until";

export class ContactsListScreen extends Component{
    constructor() {
        super();
        this.state = {
            sections: [],       //section数组
            listData: [],       //源数组
            letterArr: [],      //首字母数组

            showIndex: -1,      //显示按钮的item对应的userId
        };
    }

    componentDidMount(){
        this.getContacts();
    }

    componentWillReceiveProps(nextProps){
        this.setState({showIndex : -1});    //每次重新进入联系人页面时没有选择任何人(不会显示图标)
    }

    async getContacts() {
        try{
            let data = await API.app.contactlist();     //获取联系人列表
            const {list} = data;

            let sections = [], letterArr = [];
            list.map((item,index) => {
                if(letterArr.indexOf(item.spell.charAt(0)) === -1){
                    letterArr.push(item.spell.charAt(0) );          //得到所有不重复的首字母
                }
            });
            letterArr.sort();

            letterArr.map((item,index) => {
                const module = list.filter((it) => {         //遍历获取每一个首字母对应联系人
                        return it.spell.charAt(0) === item;
                    }
                );
                sections.push({key: item, title: item, data: module,});     //首字母对应联系人数组放到sections(每一次循环放一次)
            });

            this.setState({
                listData : list,
                letterArr,
                sections,
            });
        }catch (err) {
            console.log('err',err);
            showToast(err.message);
        }
    }

    _onSectionselect = (key) => {
        let offset = key * 20;      //点击了那个字母索引,没有section的header的高是20;key为0是即偏移0
        const {sections} = this.state;
        sections.map((item,index) => {
            if(key > index){        //要滚动的距离就是,点击的字母索引之前的所有内容,所以当 点击字母的索引 大于 sections(变量)的索引时;技术要滚动的高度
                offset = offset + item.data.length*70 + (item.data.length-1);      //每个联系人的item高是60,上下padding各为10;然后每个节点里面有length-1条分割线且高度为1
            }
        });

        //滚动到指定的偏移的位置
        this.refs._sectionList.scrollToOffset({animated: true, offset: offset});
    };

    clickItem(flag, item){
        if(flag === 'phone'){
            Linking.openURL("tel:" + item.mobile);
        }else if(flag === 'note'){
            Linking.openURL("smsto:" + item.mobile);
        }else{
            Linking.openURL("mailto:" + item.email);
        }

    }

    _renderSectionHeader(sectionItem){
        const {section} = sectionItem;
        return(
            <View style={{height:20,backgroundColor:'#e7f0f9',paddingHorizontal:10,flexDirection:'row',alignItems:'center'}}>
                <Text style={{fontSize: 16}}>{section.title.toUpperCase()}</Text>
            </View>
        );
    }

    _renderItem(item, index){
        const {showIndex} = this.state;
        return(
          <TouchableOpacity style={{paddingLeft:20,paddingRight:30,height:70,flexDirection:'row',justifyContent:'flex-start',alignItems:'center'}}
                            activeOpacity={.75}
                            onPress={() => {
                                this.setState({
                                    showIndex : item.userId,
                                });
                            }}
          >
              {item.avatar ?
                  <Image source={{uri:makeImgUrl(item.avatar)}} style={{height:50, width:50}} />
                  :
                  <Image source={require('../assets/img/headImg_def.png')} style={{height:50, width:50}} />
              }
              <View style={{marginLeft:10,flexDirection:'row',justifyContent:'space-between',flexGrow:1}}>
                  <View>
                      <Text style={styles.nameStyle}>{item.name}</Text>
                      <Text style={{fontSize:12}}>{item.department}</Text>
                      <Text style={{fontSize:12}}>{item.roleName}</Text>
                  </View>

                  {showIndex === item.userId ?
                      <View style={{flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', width: 100}}>
                          <TouchableOpacity activeOpacity={.75} style={styles.btnStyle} onPress={() => {
                              this.clickItem('phone', item)
                          }}>
                              {/*<Icon name='ios-call' style={styles.iconStyle} />*/}
                              <Image resizeMode='cover' source={require('../assets/img/tel_icon.png')}
                                     style={styles.iconImg}/>
                          </TouchableOpacity>
                          <TouchableOpacity activeOpacity={.75} style={styles.btnStyle} onPress={() => {
                              this.clickItem('note', item)
                          }}>
                              {/*<Icon name='md-chatboxes' style={styles.iconStyle} />*/}
                              <Image resizeMode='cover' source={require('../assets/img/note_icon.png')}
                                     style={styles.iconImg}/>
                          </TouchableOpacity>
                          {item.email ?
                              <TouchableOpacity activeOpacity={.75} style={styles.btnStyle} onPress={() => {
                                  this.clickItem('email', item)
                              }}>
                                  {/*<Icon name='ios-mail' style={styles.iconStyle} />*/}
                                  <Image resizeMode='cover' source={require('../assets/img/email_icon.png')}
                                         style={styles.iconImg}/>
                              </TouchableOpacity>
                              : null
                          }
                      </View>
                      :
                      null
                  }
              </View>
          </TouchableOpacity>
        );
    }

    render(){
        const {listData, letterArr, sections} = this.state;
        //偏移量 = (设备高度 - 字母索引高度 - 底部导航栏 - 顶部标题栏 - 24)/ 2
        const top_offset = (Dimensions.get('window').height - letterArr.length*22 - 52 - 44 - 24) / 2;

        return(
            <Container>
                <HeaderDark>
                    <HeaderLeft>
                    </HeaderLeft>
                    <HeaderBody>
                        <HeaderCaption>通讯录</HeaderCaption>
                    </HeaderBody>
                    <HeaderRight>
                    </HeaderRight>
                </HeaderDark>
                <Content contentContainerStyle={{flex:1,flexDirection:'row',justifyContent:'flex-start'}}>
                    <SectionList
                        ref="_sectionList"
                        renderSectionHeader={this._renderSectionHeader}
                        ItemSeparatorComponent={() => <View style={{height: 1, backgroundColor: '#E3E3E3',marginHorizontal:10}}/>}
                        sections={sections}
                        keyExtractor={(item, index) => index}
                        numColumns={1}
                        renderItem={({item, index}) => this._renderItem(item, index)}
                    />
                    <View style={{position:'absolute',width:26,right:0,top:top_offset}}>
                    {/*<View style={{width:20,flexDirection:'row',justifyContent:'center',alignItems:'center',backgroundColor:'#a3d0ee'}}>*/}
                        <FlatList
                            data= {letterArr}
                            keyExtractor = {(item, index) => index.toString()}       //不重复的key
                            renderItem={({item,index}) =>
                                <TouchableOpacity style={{marginVertical:2,height:18,flexDirection:'row',justifyContent:'center',alignItems:'center'}}
                                                  onPress={()=>{this._onSectionselect(index)}}
                                >
                                    <Text style={{fontSize:12}}>{item.toUpperCase()}</Text>
                                </TouchableOpacity>
                            }
                        />
                    {/*</View>*/}
                    </View>
                </Content>
            </Container>
        )
    }
}

const styles = StyleSheet.create({
    nameStyle: {
        fontSize: 14,
        color: '#1a1a1a',
    },
    userStyle: {
        fontSize: 12,
    },
    iconStyle: {
        color: '#4597cd',
        fontSize: 24,
    },
    iconImg:{
        width: 20,
        height: 20,

    },
    btnStyle: {
        width:30,
        height: 24,
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        marginHorizontal: 2,
    }

});

最后效果:

参考博客:https://www.jianshu.com/p/09dd60d7b34f

                  https://blog.csdn.net/u011272795/article/details/74359305

SectionList组件官方文档:https://reactnative.cn/docs/0.55/sectionlist/

文章仅为本人学习过程的一个记录,仅供参考,如有问题,欢迎指出

对博客文章的参考,若原文章博主介意,请联系删除!请原谅

猜你喜欢

转载自blog.csdn.net/halo1416/article/details/82148873