前言:最近开发的app中有一个类似手机通讯录的组件,准备自己用 SectionList 做;
注意:本文使用的RN版本为0.55版本,从官方文档上看SectionList是从0.43版本才有的。如果需要使用SectionList,请注意RN的版本问题。
首先,看下最后的效果图:
其次,接口请求的数据结构(mock模拟):
然后,看一下官方对SectionList数据格式的要求:https://reactnative.cn/docs/0.55/sectionlist/
接下来,进入正题:
1. 定义三个state,存储数据
其实两个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(
}
sections={sections}
keyExtractor={(item, index) => index}
numColumns={1}
renderItem={({item, index}) => this._renderItem(item, index)}
/>
index.toString()} //不重复的key
renderItem={({item,index}) =>
{item.toUpperCase()}
}
/>
);
注意:我这里为了把右侧的字母索引列表悬浮在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(
{section.title.toUpperCase()}
);
}
_renderItem(item, index){
const {showIndex} = this.state;
return(
{
this.setState({
showIndex : item.userId,
});
}}
>
{item.avatar ?
:
}
{item.name}
{item.department}
{item.roleName}
{showIndex === item.userId ?
{
this.clickItem('phone', item)
}}>
{/* */}
{
this.clickItem('note', item)
}}>
{/* */}
{item.email ?
{
this.clickItem('email', item)
}}>
{/* */}
: null
}
:
null
}
);
}
render(){
const {listData, letterArr, sections} = this.state;
//偏移量 = (设备高度 - 字母索引高度 - 底部导航栏 - 顶部标题栏 - 24)/ 2
const top_offset = (Dimensions.get('window').height - letterArr.length*22 - 52 - 44 - 24) / 2;
return(
通讯录
}
sections={sections}
keyExtractor={(item, index) => index}
numColumns={1}
renderItem={({item, index}) => this._renderItem(item, index)}
/>
{/**/}
index.toString()} //不重复的key
renderItem={({item,index}) =>
{this._onSectionselect(index)}}
>
{item.toUpperCase()}
}
/>
{/* */}
)
}
}
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/
文章仅为本人学习过程的一个记录,仅供参考,如有问题,欢迎指出
对博客文章的参考,若原文章博主介意,请联系删除!请原谅