ReactNative开发——滑动组件
环境
window android react-native 0.45
ScrollView
介绍
ScrollView是一个可以滑动的组件,它内部可以是一个高度不受控制的View,但它自身必须要有个固定的高度。这里如果我们不给直接他设置高度,它的上层空间有固定高度的话也是可以的。
<ScrollView>
VS <FlatList>
我们应该选择哪个?
ScrollView
的工作是简单的渲染所有的子组件,它的用法比较简单。
FlatList
是惰性渲染item,只有当item出现在界面上时才渲染,并且从屏幕滑出去之后该item会被移除。所以它更省内存,也更节省cpu处理的时间。当然他的用法也比较复杂。
用法
我们来看一下用法示例,大家可以直接copy我的代码运行试试看:
/**
* Created by blueberry on 6/9/2017.
* @flow
*/
import React, {Component} from 'react';
import {
AppRegistry,
StyleSheet,
View,
Text,
ScrollView,
ListView,
FlatList,
RefreshControl,
Dimensions,
Button,
TextInput,
} from 'react-native';
let totalWidth = Dimensions.get('window').width;
let totalHeight = Dimensions.get('window').height;
export default class MainPage extends Component {
state = {
isRefresh: false,
}
_onRefresh() {
console.log('onRefresh.');
this.setState({isRefresh: true});
// 模拟获取数据需要三秒
setTimeout(() => this.setState({isRefresh: false}), 3000);
}
_onScroll() {
console.log('onScroll.');
}
render() {
return (
<View style={styles.container}>
<Button title="滑动到底部" onPress={() => _outScrollView.scrollToEnd({animated: false})}/>
<ScrollView
ref={(scrollView) => {
_outScrollView = scrollView;
}}
contentContainerStyle={styles.outScrollView}
onScroll={this._onScroll} //回调
scrollEventThrottle={100} // ios : 控制scroll回调的频率,没秒触发多少次
showsVerticalScrollIndicator={false} //设置不显示垂直的滚动条
keyboardDismissMode={'on-drag'} // 'none'默认值,滑动时不隐藏软件盘,
// ‘on-drag'滑动时隐藏软件盘.interactive :ios可用。上滑可以回复键盘
keyboardShouldPersistTaps={'always'} //'never'默认值,点击TextInput以外的组件,软键盘收起,
// 'always'不会收起,`handle` 当点击事件被子组件捕获时,
//键盘不会自动收起,但我用android测试了,发现没有效果
//我使用的版本:RectNative 0.45
refreshControl={
<RefreshControl refreshing={this.state.isRefresh}
onRefresh={this._onRefresh.bind(this)}
title={'load...'}
tintColor={'#ff0000'}
colors={['#ff0000', '#00ff00', '#0000ff']}
progressBackgroundColor={'#ffff00'}
/>
}
>
<ScrollView horizontal={true} contentContainerStyle={styles.inScrollView}
showsHorizontalScrollIndicator={false} //不显示滑动条
>
<TextInput placeholder={'测试软键盘'}
style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: '#fff1ae'}}/>
<View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
</ScrollView>
<ScrollView horizontal={true} contentContainerStyle={styles.inScrollView}>
<TextInput placeholder={'测试软键盘'}
style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: '#fff1ae'}}/>
<View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
</ScrollView>
<ScrollView horizontal={true} contentContainerStyle={styles.inScrollView}>
<View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'red'}}/>
<View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
</ScrollView>
<ScrollView horizontal={false} contentContainerStyle={styles.inScrollView}>
<View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'red'}}/>
<View style={{width: totalWidth, height: 0.5 * totalHeight, backgroundColor: 'blue'}}/>
</ScrollView>
</ScrollView>
</View>
);
}
}
var
styles = StyleSheet.create({
container: {
flex: 1,
height: '50%',
backgroundColor: 'grey',
},
outScrollView: {
// flex: 1, 这里指定flex的话,会出现不能上下滑动,原因在这样会把 "内容高度定死了",所以最好不要设置高度/flex,让内容的高度自适应
justifyContent: 'center',
backgroundColor: 'green',
},
inScrollView: {
padding: 20,
backgroundColor: '#88ff73'
}
});
AppRegistry
.registerComponent(
'Project08'
, () =>
MainPage
)
;
上面的代码创建了一个ScrollView
其中嵌套了4个ScrollView
,有3个是横向滑动,最后一个是纵向滑动。ps:这里竟然没有滑动冲突,我想说:“666”,这要是android原生开的话,这种布局可是比较麻烦的。
基本属性
属性 | 作用 |
---|---|
contentContainerStyle | 设置内层容器的样式。what?什么是内层容器?这里我的理解是,它这个ScroolView中还包装着一个View,这里View包含有我们设置的Item,大家想想,我们在android原生开发中使用呢ScrollView的时候,内层是不是一般也要嵌套一个LinearLayout用来存放子View吧 |
onScroll | 回调方法,滑动的时候回调 |
scrollEventThrottle | 这个之后ios有效,用来设置onScroll滑动的频率,可以节省性能,类型:number.表示1秒回调多少次 |
showsVerticalScrollIndicator | 这个用来设置是否显示垂直滚动条,和他相似的还有showsHorizontalScrollIndicator |
showsHorizontalScrollIndicator | 用来设置是否显示横向滑动条 |
keyboardDismissMode | 用来设置软件盘滑动的时候,是否隐藏的模式,none(默认值),拖拽时不隐藏软键盘 on-drag 当拖拽开始的时候隐藏软键盘 interactive 软键盘伴随拖拽操作同步地消失,并且如果往上滑动会恢复键盘。安卓设备上不支持这个选项,会表现的和none一样 |
keyboardShouldPersistTaps | ‘never’(默认值),点击TextInput以外的子组件会使当前的软键盘收起。此时子元素不会收到点击事件。’always’,键盘不会自动收起,ScrollView也不会捕捉点击事件,但子组件可以捕获 ‘handled’,当点击事件被子组件捕获时,键盘不会自动收起。这样切换TextInput时键盘可以保持状态。多数带有TextInput的情况下你应该选择此项,但我用android机测试的时候,发现没卵用 |
refreshControl | 用来设置下拉刷新组件,这个组件下文将介绍 |
ok,这些是基本属性,更多属性大家可以参考:http://reactnative.cn/docs/0.45/scrollview.html#content
RefreshControl
这个组件我上文的代码中,大家应该已经看到用法了。
refreshControl={
<RefreshControl refreshing={this.state.isRefresh}
onRefresh={this._onRefresh.bind(this)}
title={'load...'}
tintColor={'#ff0000'}
colors={['#ff0000', '#00ff00', '#0000ff']}
progressBackgroundColor={'#ffff00'}
/>
}
需要的属性基本我都写上了,这里我再列个表格解释一下好了。
属性 | 作用 |
---|---|
refreshing | bool 类型,如果设置true,则下拉刷新按钮就一直显示着,如果设置false,就不显示,只有当下拉的时候显示 |
onRefresh | 下拉回调 |
title | 标题 |
tintColor | 指定刷新指示器的颜色 |
colors | 指定至少一种颜色用来绘制刷新指示器 |
progressBackgroundColor | 指定刷新指示器的背景色 |
ListView
ListView是一个可以垂直滑动的组件,一般用来显示列表数据
用法
/**
* Created by blueberry on 6/9/2017.
* @flow
*/
import React, {Component} from 'react';
import {AppRegistry, StyleSheet, View, ListView, Text, Button} from 'react-native';
import StaticContainer from './StaticContainer';
let array = [];
{
let len = 100;
for (let i = 0; i < len; i++) {
array.push('测试数据' + i);
}
}
/**
* 加个log,用来测试,是否更新。
*/
class LogView extends Component {
componentDidUpdate() {
console.log(this.props.name + 'Did update');
}
render() {
return (
<Text style={{backgroundColor: '#ffd98c'}}>
我是:{this.props.name}
</Text>
);
}
}
export default class ListViewPage extends Component {
constructor() {
super();
let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
//填充数据
dataSource: ds.cloneWithRows(array),
};
}
render() {
return (
<ListView
// 数据源
dataSource={this.state.dataSource}
initialListSize={10} //初始的时候显示的数量
onChangeVisibleRows={(visible, changedRows) => {
// 我用android测试,没有回调....
// visible: 类型:{ sectionID: { rowID: true }}
// { sectionID: { rowID: true | false }}
console.log('visible:' + JSON.stringify(visible));
console.log('changedRow:' + JSON.stringify(changedRows));
}}
onEndReached={() => console.log('onEndReached')}//当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足
// onEndReachedThreshold个像素的距离时调用。原生的滚动事件会被作为参数传递。译注:当第一次渲染时,
// 如果数据不足一屏(比如初始值是空的),这个事件也会被触发,请自行做标记过滤。
onEndReachedThreshold={2} //调用onEndReached之前的临界值,单位是像素
pageSize={3} //每次渲染的行数
// 返回一个头部可以渲染的组件
renderHeader={() => (
<LogView name="header"/>
)}
//返回一个尾部可以渲染的组件
renderFooter={() => (
<StaticContainer>
<LogView name="Footer"/>
</StaticContainer>
)}
//显示每一行
renderRow={
(rowData, sectionID, rowID, highlightRow) => {
return ( <Text
style={{borderBottomColor: 'grey', borderBottomWidth: 1}}>
{'rowData:' + rowData + ' sectionId:' + sectionID + " rowId:" + rowID + " highlightRow:" + highlightRow}
</Text>
)
}}/>
);
}
}
AppRegistry.registerComponent('Project08', () => ListViewPage);
上面的代码实现了一个用来显示100条数据的列表,它还有一个头部,和一个尾部,因为头部和尾部的数据一般都不收布局变化,所有使用了一个StaticContainer
来包装它,让他不刷新。这样做可以提高效率。为了看出效果,我特意定义了一个LogView
组件,用来测试。
ListView.DataSource
ListView.DataSource 主要用来为ListView提供数据,它的一般用法。上面的代码已经给出了。
let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
//填充数据
dataSource: ds.cloneWithRows(array),
};
它还有另外一个方法:cloneWithRowsAndSections(dataBlob, sectionIdentities, rowIdentities)
用来填充分组数据。
使用详细可以参考:http://reactnative.cn/docs/0.45/listviewdatasource.html#content
基本属性
属性名 | 作用 |
---|---|
dataSource | 数据源,上文已经说明 |
initialListSize | 初始的时候显示的数量 |
onChangeVisibleRows | 当可见的行的集合变化的时候调用此回调函数。visibleRows 以 { sectionID: { rowID: true }}的格式包含了所有可见行,而changedRows 以{ sectionID: { rowID: true |
onEndReached | 当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足onEndReachedThreshold个像素的距离时调用。原生的滚动事件会被作为参数传递。译注:当第一次渲染时,如果数据不足一屏(比如初始值是空的),这个事件也会被触发,请自行做标记过滤。 |
onEndReachedThreshold | 调用onEndReached之前的临界值,单位是像素。 |
pageSize | 每次事件循环(每帧)渲染的行数。 |
renderFooter | 页头与页脚会在每次渲染过程中都重新渲染(如果提供了这些属性)。如果它们重绘的性能开销很大,把他们包装到一个StaticContainer或者其它恰当的结构中。页脚会永远在列表的最底部,而页头会在最顶部。 |
renderHeader | 和renderFoot的用法一样 |
renderRow | (rowData, sectionID, rowID, highlightRow) => renderable 最重要的方法,用渲染每个item,其中rowData是你定义的数据列表中的类型数据,sectionID是该行的sectionID,rowId是该行的rowID,hightlighRow是一个函数引用,我目前没有发现卵用,官网说:如果item正在被高亮,可以通过hightlightRow(null) 来重置 |
scrollRenderAheadDistance | 当一个行接近屏幕范围多少像素之内的时候,就开始渲染这一行 |
renderSectionHeader | (sectionData, sectionID) => renderable 如果提供了此函数,会为每个小节(section)渲染一个粘性的标题。 |
stickySectionHeadersEnabled设置小节标题(section header)是否具有粘性 | |
stickyHeaderIndices | 一个子视图下标的数组,用于决定哪些成员会在滚动之后固定在屏幕顶端 |
额,还有ScrollView有的属性,ListView都有;所有horizontal、refreshControl,都可以个ListView设置。
基本属性就这些,更多属性参考:http://reactnative.cn/docs/0.45/listview.html#content
分组显示
/**
* Created by blueberry on 6/9/2017.
*
* @flow
*/
import React, {Component} from 'react';
import {AppRegistry, StyleSheet, View, ListView, Text} from 'react-native';
let array: Array<Array<string>> = [];
{
for (let i = 0; i < 10; i++) {
let group: Array<string> = [];
for (let j = 0; j < 10; j++) {
group.push('分组:' + i + " item:" + j);
}
array['分组' + i] = group;
}
}
export default class GroupListView extends Component {
constructor() {
super();
var ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (pre, next) => pre !== next,
});
this.state = {
/**
* 填充数据
* @params 所有的分组数据,结构{分组1:[分组1 item1,分组1item2. ...],分组2:[分组2item1,....]}
* @param sectionIdentities 每个分组的索引
*/
dataSource: ds.cloneWithRowsAndSections(array,
Object.keys(array)),
};
}
render() {
return (
<ListView dataSource={this.state.dataSource} renderRow={(rowData) => {
return <Text>{rowData}</Text>;
}}
renderSectionHeader={(sectionData, sectionId) => {
console.log('sectionData:' + JSON.stringify(sectionData) + ' , sectionID:' + JSON.stringify(sectionId));
return <Text style={{backgroundColor: 'red'}}>{sectionData[0]}</Text>
}}
stickySectionHeadersEnabled={true} //开启之后,会有个粘性效果,
stickyHeaderIndices={[1]} //一个子视图下标的数组(这个下标连section也算在内的,),用于决定哪些成员会在滚动之后固定在屏幕顶端.根
// stickySectionHeadersEnabled的效果很像
scrollRenderAheadDistance={10} //当一个行接近屏幕范围多少像素之内的时候,就开始渲染这一行。
/>
);
}
}
AppRegistry.registerComponent('Project08', () => GroupListView);
上面代码实现了用listView分组显示item。
FlatList
FlatList是ListView的升级版,它的性能比ListView好一些,但它目前刚出来,冒死还有些坑存在。。。
用法
/**
* Created by blueberry on 6/11/2017.
*/
import React, {Component, PureComponent} from 'react';
import {AppRegistry, StyleSheet, View, FlatList, Text, TouchableOpacity} from 'react-native';
class ListItem extends PureComponent {
_onPress = () => {
this.props.onPressItem(this.props.id);
}
render() {
let color = this.props.selected ? 'red' : 'blue';
return (
<TouchableOpacity
style={{height: 200, justifyContent: 'center', flex: 1, alignItems: 'center', backgroundColor: color}
} onPress={this._onPress}>
<Text style={{fontSize: 20, height: 100,}}>{this.props.title}</Text>
<Text style={{fontSize: 18, height: 80}}>{this.props.content}</Text>
</TouchableOpacity>
);
}
}
/**
* PureComponent 可以提高性能,只有在props或state发生改变时render。
*/
export default class FlatListPage extends PureComponent {
state = {selected: (new Map(): Map<string, boolean>)};
_onPressItem = (id: string) => {
this.setState((state) => {
const selected = new Map(state.selected);
selected.set(id, !selected.get(id));//toggle.
return {selected};
});
};
_keyExtractor = (item, index) => item.id;
/**
* 使用箭头函数,既保证了this指向FlatListPage,也保证了不会每次都生成一个新的函数,这样在对比prop时,就返回'没有改变'
*/
_renderItem = ({item}) => (
<ListItem
id={item.id}
onPressItem={this._onPressItem}
selected={!!this.state.selected.get(item.id)}
title={item.title}
content={item.content}
/>
);
render() {
return (
<FlatList
data={this.props.data}
renderItem={this._renderItem}
// extraData={this.state}
keyExtractor={this._keyExtractor}
ItemSeparatorComponent={() => <View style={{height: 2, backgroundColor: 'black'}}/>} //分割线
ListFooterComponent={() => <View style={{height: 50, backgroundColor: 'red'}}/>} //尾部布局
ListHeaderComponent={() => <View style={{height: 50, backgroundColor: 'blue'}}/>} //头部布局
columnWrapperStyle={{height: 200, backgroundColor: 'green',}}
numColumns={2} //
//getItemCount={40}
//getItemLayout={(data, index) => ({length: 200, offset: 200 * index, index})}
refreshing={false}
onEndReachedThreshold={20} //决定距离底部20个单位的时候,回到onEndReacted,但是我这只20,
// 他距离4000左右的时候就回掉了,测试版本Android reactNative Api:0.45
onEndReached={(info) => {
console.log('onEndReacted:' + info.distanceFromEnd);
}}
/>
);
}
}
{
let array = [];
for (let i = 0; i < 40; i++) {
array[i] = {id: i, key: 'key' + i, title: '标题' + i, content: '内容' + i,};
}
FlatListPage.defaultProps = {data: array};
}
AppRegistry.registerComponent('Project08', () => FlatListPage);
常用属性
属性 | 作用 |
---|---|
ItemSeparatorComponent | 行与行之间的分隔线组件。不会出现在第一行之前和最后一行之后 |
ListFooterComponent | 设置尾部组件 |
ListHeaderComponent | 设置头部组件 |
columnWrapperStyle | 如果设置了多列布局(即将numColumns值设为大于1的整数),则可以额外指定此样式作用在每行容器上。 |
data | 为了简化起见,data属性目前只支持普通数组。如果需要使用其他特殊数据结构,例如immutable数组,请直接使用更底层的VirtualizedList组件。 |
extraData | 如果有除data以外的数据用在列表中(不论是用在renderItem还是Header或者Footer中),请在此属性中指定。同时此数据在修改时也需要先修改其引用地址(比如先复制到一个新的Object或者数组中),然后再修改其值,否则界面很可能不会刷新。 |
keyExtractor此函数用于为给定的item生成一个不重复的key。Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为key值。若item.key也不存在,则使用数组下标 | |
numColumns | 多列布局只能在非水平模式下使用,即必须是horizontal={false}。此时组件内元素会从左到右从上到下按Z字形排列,类似启用了flexWrap的布局。组件内元素必须是等高的——暂时还无法支持瀑布流布局 |
更多属性请参考: http://reactnative.cn/docs/0.45/flatlist.html#content
上述代码定义的组件都继承了 PureComponent,这个组件的作用是,只有prop和state它才render。实现它是为了提高效率。
SectionList
是一个高性能的分组列表组件
使用
/**
* Created by blueberry on 6/12/2017.
*/
import React, {Component, PureComponent} from 'react';
import {AppRegistry, StyleSheet, View, SectionList, Text, RefreshControl} from 'react-native';
class ListItem extends PureComponent {
render() {
return (
<Text style={{backgroundColor: 'red', height: 100}}>{this.props.title}</Text>
);
}
}
let sections = [];
for (let i = 0; i < 10; i++) {
data = [];
for (let j = 0; j < 10; j++) {
data[j] = {title: '分组' + i + ',item' + j, id: j};
}
sections.push({data: data, key: '分组' + i});
// 也可以使用下面方式自定义 不同section渲染不同类型的子组件
//sections.push({data: data, key: '分组' + i, renderItem: i === 2 ? ()=><ListItem title="测试"/> : undefined});
}
export default class SectionListPage extends PureComponent {
//为每一行生成唯一的key
_keyExtractor = (item, index) => '' + item.key + index;
render() {
console.log(JSON.stringify(sections));
return (
<SectionList
//渲染item的组件
renderItem={({item}) =>
<ListItem
title={item.title}
/>
}
//渲染sectionHeader的组件
renderSectionHeader={({section}) =>
<Text>{section.key}</Text>
}
//数据
sections={sections}
//生成唯一的key
keyExtractor={this._keyExtractor}
ItemSeparatorComponent={() => <View style={{height: 2, backgroundColor: 'black'}}/>} //分割线
ListFooterComponent={() => <View style={{height: 50, backgroundColor: 'red'}}/>} //尾部布局
ListHeaderComponent={() => <View style={{height: 50, backgroundColor: 'blue'}}/>} //头部布局
/>
);
}
}
AppRegistry.registerComponent('Project08', () => SectionListPage);
基本属性
属性 | 作用 |
---|---|
renderItem | 和FlatList的renderItem作用一样。用来设置渲染item的组件,但SectionList,也可以section数据源中设置renderItem这个属性 |
renderSectionHeader | 设置渲染分组小标签的组件 |
seciton | 设置数据源 |
其余属性和FlatList
中的属性作用一样,这里就不在介绍了。
我在上述代码中有这么一行,用来设置不同的renderItem函数,读者可以去掉注释看看效果
// sections.push({data: data, key: '分组' + i, renderItem: i === 2 ? ()=><ListItem title="测试"/> : undefined});
总结
其实我们文章中我们主要提到了 6个组件:ScrollView
ListView
RefreshControl
FlatList
SectionList
PureComponent
;其中主要讲解了四个滑动组件
- ScrollView
它没有懒加载功能,适合少量数据显示,用法比较简单。
- ListView
它用来显示列表数据,是懒加载Item,支持下拉刷新,上拉加载应该用它的onEndReached
也是可以办到的。也支持分组列表显示,分组标签粘性滑动等功能,性能比较好。设置数据需要结合ListView.DataSource组件。
- FlatList
可以说是ListView的升级版,性能比ListView要好,同样支持下拉刷新等功能,目前我用0.45版本,刚出来,官方说还不稳定。
- SectionList
用来显示分组列表组件,性能比较高。
上面就是本文介绍的四个滑动组件,他们都支持下拉刷新组件,(ScrollView)有的属性,其他滑动组件基本都有。
RefreshControl
就是官方给出的下拉刷新组件,用来设置到滑动组件上。
PureComponent
之后当state或props变了之后,才能刷新。可以提高性能。
ok,介绍到这里了,其中我写的测试源码,在上文都贴出来了,大家可以测试测试。
参考
ReactNative 官网:http://facebook.github.io/react-native/releases/0.43/docs/flatlist.html
ReactNative 中文网:http://reactnative.cn/docs/0.45/sectionlist.html#content