1.背景
本文使用的RN(使用RN表示React Native)的版本為0.44版本,從官方文檔上看SectionList是從0.43版本才有的,而要列表的吸頂懸浮功能,從0.44版本才開始有。具體可以查看React Native的官方文檔。至于為何使用SectionList而不是使用ListView,可以自行百度SectionList(FlatList)的好處,就我了解主要是性能上的差別(這點深受ListView其害)。這里就不加以討論了,本文主要介紹的是如何使用SectionList打造分組懸停,并且添加右側的分組的跳轉控制(類似微信的通訊錄)
2.SectionList簡單介紹
首先我們要做的是,如何使用SectionList生成一個分組列表,具體的使用我們可以看下RN官方中文文檔http://reactnative.cn/docs/0.44/sectionlist.html 其中對SectionList和FlatList的介紹,我們來看看數據源的格式
<SectionList
renderItem={({item}) => <ListItem title={item.title} />}
renderSectionHeader={({section}) => <H1 title={section.key} />}
sections={[ // 不同section渲染相同類型的子組件
{data: [{key:...}...], key: ...},
{data: [{key:...}...], key: ...},
{data: [{key:...}...], key: ...},
]}
/>
在這里我們重點要注意的是,對于每組數據,必須有一個key的字段,并且不同組之間key的內容必須是不一樣的,同時每組里面的數據也必須有一個key的字段,key的內容同樣是不一致的。(雖然我也不曉得為嘛要這樣設計,但是實際使用過程中不添加的話的確會報warning,有興趣的話可以看下其中的源碼)
3.SectionList生成城市列表
3.1 格式化城市列表數據
這里我們有一個city.json的數據源,里面包含了國內的城市列表信息,格式組成如下
{
"data": [
{
"title": "A"
"city": [
{
"city_child": "阿壩",
"city_child_en": "aba",
"city_id": 101271901,
"city_name_ab": "ab.ab",
"city_parent": "阿壩",
"city_pinyin_name": "aba.aba",
"country": "中國",
"latitude": 32,
"longitude": 101,
"provcn": "四川"
},
{
"city_child": "阿巴嘎",
"city_child_en": "abaga",
"city_id": 101080904,
"city_name_ab": "xlgl.abg",
"city_parent": "錫林郭勒",
"city_pinyin_name": "xilinguole.abaga",
"country": "中國",
"latitude": 44,
"longitude": 114,
"provcn": "內蒙古"
},
.....]
},
.......
]
}
現在我們要做的就是把這樣的數據源格式化成我們SectionList需要的數據的格式,格式化代碼如下
async getCityInfos() {
let data = await require('../app/assets/city.json');
let jsonData = data.data
//每組的開頭在列表中的位置
let totalSize = 0;
//SectionList的數據源
let cityInfos = [];
//分組頭的數據源
let citySection = [];
//分組頭在列表中的位置
let citySectionSize = [];
for (let i = 0; i < jsonData.length; i++) {
citySectionSize[i] = totalSize;
//給右側的滾動條進行使用的
citySection[i] = jsonData[i].title;
let section = {}
section.key = jsonData[i].title;
section.data = jsonData[i].city;
for (let j = 0; j < section.data.length; j++) {
section.data[j].key = j
}
cityInfos[i] = section;
//每一項的header的index
totalSize += section.data.length + 1
}
this.setState({data: cityInfos, sections: citySection, sectionSize: citySectionSize})
}
在這里我們用async async,然后異步讀取city.json里面的數據,然后遍歷整個數據我們得到三組我們需要的數據,分別為SectionList的數據源(列表展示使用),分組頭的數據源(后面我們在右側展示是使用),分組頭在列表中的位置(做列表跳轉的時候使用)。
??這樣我們得到了數據源,然后將其添加到SectionList中,我們來看下效果
<SectionList
ref='list'
enableEmptySections
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
sections={this.state.data}
getItemLayout={this._getItemLayout}/>
3.2 列表右側分組頭展示
在上面中我們得到了分組的頭的列表citySection,那么我們改如和將其顯示到列表右側呢?
??在這里我們將頭部使用Text進行展示,然后外部使用View進行包裹,對外部的View進行手勢監聽,根據位置和距離來判斷當前選中的頭部,然后通知SectionList進行相對應的操作。
??首先我們生成Text,然后對其進行高度測量(便于之后的手勢控制使用)
_getSections = () => {
let array = new Array();
for (let i = 0; i < this.props.sections.length; i++) {
array.push(
<View
style={styles.sectionView}
pointerEvents="none"
key={i}
ref={'sectionItem' + i}>
<Text
style={styles.sectionItem}>{this.props.sections[i]}</Text>
</View>)
}
return array;
}
componentDidMount() {
//它們的高度都是一樣的,所以這邊只需要測量一個就好了
const sectionItem = this.refs.sectionItem0;
this.measureTimer = setTimeout(() => {
sectionItem.measure((x, y, width, height, pageX, pageY) => {
this.measure = {
y: pageY,
height
};
})
}, 0);
}
由于它們每一項的高度是一樣的,所以這邊只需要測量一個的高度,其他的也就都知道了
??然后我們將這些Text展示到View中去,并對View進行手勢控制的監聽
<View
style={styles.container}
ref="view"
onStartShouldSetResponder={returnTrue}
onMoveShouldSetResponder={returnTrue}
onResponderGrant={this.detectAndScrollToSection}
onResponderMove={this.detectAndScrollToSection}
onResponderRelease={this.resetSection}>
{this._getSections()}
</View>
const returnTrue = () => true;
從代碼中我們可以看出,這邊我們需要處理的是手勢的Move和抬起事件,那么首先我們來看Move的操作。
detectAndScrollToSection = (e) => {
var ev = e.nativeEvent.touches[0];
// 手指按下的時候需要修改顏色
this.refs.view.setNativeProps({
style: {
backgroundColor: 'rgba(0,0,0,0.3)'
}
})
let targetY = ev.pageY;
const {y, height} = this.measure;
if (!y || targetY < y) {
return;
}
let index = Math.floor((targetY - y) / height);
index = Math.min(index, this.props.sections.length - 1);
if (this.lastSelectedIndex !== index && index < this.props.sections.length) {
this.lastSelectedIndex = index;
this.onSectionSelect(this.props.sections[index], index, true);
this.setState({text: this.props.sections[index], isShow: true});
}
}
onSectionSelect(section, index, fromTouch) {
this.props.onSectionSelect && this.props.onSectionSelect(section, index);
if (!fromTouch) {
this.lastSelectedIndex = null;
}
}
從代碼中我們可以知道,首先我們要對View的背景顏色進行改變,這樣可以讓我們知道已經選中了該View了,然后獲取我們當前觸摸點的坐標,在之前我們已經計算每個Text的高度,然后我們根據這些就可以計算出當前觸摸點之下的是哪個分組了。最后通過
this.onSectionSelect(this.props.sections[index], index, true); this.setState({text: this.props.sections[index], isShow: true});
分別進行外部列表的通知和當前View的通知
??然后我們來看手勢抬起時候的操作
resetSection = () => {
// 手指抬起來的時候需要變回去
this.refs.view.setNativeProps({
style: {
backgroundColor: 'transparent'
}
})
this.setState({isShow: false})
this.lastSelectedIndex = null;
this.props.onSectionUp && this.props.onSectionUp();
}
從代碼之中我們可以知道,該方法主要是處理View背景的變化,以及抬起時候的一些通知。我們先看下整體的效果
接下來就是選擇的一個提示了(類似于微信通訊錄中間的彈窗通知)。首先我們創建我們需要的一個視圖
<View
pointerEvents='box-none'
style={styles.topView}>
{this.state.isShow ?
<View style={styles.modelView}>
<View style={styles.viewShow}>
<Text style={styles.textShow}>{this.state.text}</Text>
</View>
</View> : null
}
<View
style={styles.container}
ref="view"
onStartShouldSetResponder={returnTrue}
onMoveShouldSetResponder={returnTrue}
onResponderGrant={this.detectAndScrollToSection}
onResponderMove={this.detectAndScrollToSection}
onResponderRelease={this.resetSection}>
{this._getSections()}
</View>
</View>
在這里我們要注意的是,由于我們的展示視圖是在屏幕的中間位置,并且在Android上子視圖超出父視圖的部分無法顯示(也就是設置left:-100這樣的屬性會讓視圖部分無法看見)。所以這里我們使用的包裹展示視圖的父視圖是全屏的,那么這個pointerEvents='box-none'就尤其重要,它可以保證當前視圖不操作手勢控制,而子視圖可以操作。假如這邊不設置的話,會導致SectionList無法滾動,因為被當前視圖蓋住了。具體的屬性介紹可以查看RN官方文檔對View屬性的介紹
??在上面的方法之中我們有需改state的值,這邊就是來控制展示視圖的顯示隱藏的。我們來看下效果
3.3 列表的分組跳轉
上面做到了列表的展示,接下來就是列表的分組跳轉了,在RN的介紹文檔上可以看到VirtualizedList(FlatList和SectionList都是對它的封裝)有如下的方法
scrollToEnd(params?: object)
scrollToIndex(params: object)
scrollToItem(params: object)
scrollToOffset(params: object)
從名稱上看就是對列表的滾動,然后找到FlatList,里面有更詳細的介紹,這邊我們使用的方法是scrollToIndex
scrollToIndex(params: object)
Scrolls to the item at a the specified index such that it is positioned in the viewable area such that viewPosition
0 places it at the top, 1 at the bottom, and 0.5 centered in the middle.
如果不設置getItemLayout屬性的話,可能會比較卡。
那么從最后一句話我們知道在這里我們需要去設置getItemLayout屬性,這樣就可以告訴SectionList列表的高度以及每個項目的高度了
const ITEM_HEIGHT = 50; //item的高度
const HEADER_HEIGHT = 24; //分組頭部的高度
const SEPARATOR_HEIGHT = 0; //分割線的高度
_getItemLayout(data, index) {
let [length, separator, header] = [ITEM_HEIGHT, SEPARATOR_HEIGHT, HEADER_HEIGHT];
return {length, offset: (length + separator) * index + header, index};
}
同時我們要注意的是在設置SectionList的renderItem和renderSectionHeader,也就是SectionList的分組內容和分組頭部的組件的高度必須是我們上面給定計算時候的高度(這點很重要)
??那么這個時候根據上面右側頭部展示組件給的回調的值(配合我們第一步得到的citySectionSize)就可以進行
對應的列表滾動了。
<View style={{paddingTop: Platform.OS === 'android' ? 0 : 20}}>
<View>
<SectionList
ref='list'
enableEmptySections
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
sections={this.state.data}
getItemLayout={this._getItemLayout}/>
<CitySectionList
sections={ this.state.sections}
onSectionSelect={this._onSectionselect}/>
</View>
</View>
//這邊返回的是A,0這樣的數據
_onSectionselect = (section, index) => {
//跳轉到某一項
this.refs.list.scrollToIndex({animated: true, index: this.state.sectionSize[index]})
}
但是假如僅僅只是這樣的話,你會發現在使用的時候會報錯,錯誤是找不到scrollToIndex方法。wtf??RN官方文檔上明明有這個方法啊。然而其實FlatList對VirtualizedList封裝的時候有添加這些方法,而SectionList并沒有。那么,只能自己動手添加了,參照
FlatList里面的scrollToIndex方法,為SectionList添加對于的方法。
??其中SectionList的路徑為
node_modules/react-native/Libraries/Lists/SectionList.js,代碼格式化后大概在187行的位置,修改如下
class SectionList<SectionT: SectionBase<any>>
extends React.PureComponent<DefaultProps, Props<SectionT>, void> {
props: Props<SectionT>;
static defaultProps: DefaultProps = defaultProps;
render() {
const List = this.props.legacyImplementation ? MetroListView : VirtualizedSectionList;
return <List
ref={this._captureRef}
{...this.props} />;
}
_captureRef = (ref) => {
this._listRef = ref;
};
scrollToIndex = (params: { animated?: ?boolean, index: number, viewPosition?: number }) => {
this._listRef.scrollToIndex(params);
}
}
同時還需要修改VirtualizedSectionList的代碼,路徑在node_modules/react-native/Libraries/Lists/VirtualizedSectionList.js,大概253行處修改如下
render() {
return <VirtualizedList
ref={this._captureRef}
{...this.state.childProps} />;
}
_captureRef = (ref) => {
this._listRef = ref;
};
scrollToIndex = (params: { animated?: ?boolean, index: number, viewPosition?: number }) => {
this._listRef.scrollToIndex(params);
}
修改完畢,我們來看下效果ios和android平臺下的效果如和
??從上面的效果上可以看出來ios上的效果比android上的效果要好,然后在ios上有分組懸停的效果而在andorid并沒有,這是由于平臺特性決定的。在手動滾動的時候白屏的時間較短,而在跳轉的時候白屏的時間較長,但是相比與之前ListView時期的長列表的效果而言,要好太多了。也是期待RN以后的發展中對列表更好的改進吧。
4.最后
在完成這個城市選擇列表的時候主要參考了
http://reactnative.cn/docs/0.44/sectionlist.html
http://reactnative.cn/docs/0.44/flatlist.html
https://github.com/sunnylqm/react-native-alphabetlistview
最后附上項目地址:https://github.com/hzl123456/SectionListDemo
注意要記得修改SectionList和VirtualizedSectionList的代碼