ListView 长列表
定义/Definition
react-native 核心组件。 高性能列表:节点分步渲染;无尽列表。规则 / Rule
常用在渲染长列表中代码演示
无尽列表
无尽列表 sticky
import { ListView, Toast, Button } from 'antd-mobile';const data = [{img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',title: '相约酒店',des: '不是所有的兼职汪都需要风吹日晒',},{img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',title: '麦当劳邀您过周末',des: '不是所有的兼职汪都需要风吹日晒',},{img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',title: '食惠周',des: '不是所有的兼职汪都需要风吹日晒',},];let index = data.length - 1;const NUM_SECTIONS = 5;const NUM_ROWS_PER_SECTION = 5;let pageIndex = 0;const Demo = React.createClass({getInitialState() {const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];const dataSource = new ListView.DataSource({getRowData,getSectionHeaderData: getSectionData,rowHasChanged: (row1, row2) => row1 !== row2,sectionHeaderHasChanged: (s1, s2) => s1 !== s2,});this.dataBlob = {};this.sectionIDs = [];this.rowIDs = [];this.genData = (pIndex = 0) => {for (let i = 0; i < NUM_SECTIONS; i++) {const ii = pIndex * NUM_SECTIONS + i;const sectionName = `Section ${ii}`;this.sectionIDs.push(sectionName);this.dataBlob[sectionName] = sectionName;this.rowIDs[ii] = [];for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {const rowName = `S${ii}, R${jj}`;this.rowIDs[ii].push(rowName);this.dataBlob[rowName] = rowName;}}// new object refthis.sectionIDs = [].concat(this.sectionIDs);this.rowIDs = [].concat(this.rowIDs);};this.genData();return {dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),isLoading: false,};},onEndReached(event) {// load new dataconsole.log('reach end', event);Toast.info('加载新数据');this.setState({ isLoading: true });setTimeout(() => {this.genData(++pageIndex);this.setState({dataSource: this.state.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),isLoading: false,});}, 1000);},render() {const separator = (sectionID, rowID) => (<div key={`${sectionID}-${rowID}`} style={{backgroundColor: '#F5F5F9',height: 8,borderTop: '1px solid #ECECED',borderBottom: '1px solid #ECECED',}}></div>);const row = (rowData, sectionID, rowID) => {if (index < 0) {index = data.length - 1;}const obj = data[index--];return (<div key={rowID}style={{padding: '8px 16px',backgroundColor: 'white',}}><h3 style={{padding: 2,marginBottom: 8,borderBottom: '1px solid #F6F6F6',}}>{obj.title}</h3><div style={{ display: 'flex' }}><img style={{ height: 64, marginRight: 8 }} src={obj.img} /><div><p>{obj.des}</p><p><span style={{ fontSize: 24, color: '#FF6E27' }}>35</span>元/任务</p></div></div></div>);};this.ctrlBodyScroll(false, true);return (<div style={{ margin: '0 auto', width: '96%' }}><ListViewdataSource={this.state.dataSource}renderHeader={() => <span>header</span>}renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>{this.state.isLoading ? '加载中...' : '加载完毕'}</div>}renderSectionHeader={(sectionData) => (<div>{`任务 ${sectionData.split(' ')[1]}`}</div>)}renderRow={row}renderSeparator={separator}pageSize={4}scrollRenderAheadDistance={500}scrollEventThrottle={20}onScroll={() => { console.log('scroll'); }}onEndReached={this.onEndReached}onEndReachedThreshold={10}style={{ height: 300, overflow: 'auto', border: '1px solid #ddd', margin: '10px 0' }}/><div><p>切换`body`的`overflow`样式:</p><Button inline size="small" onClick={() => { this.ctrlBodyScroll(true); }}>auto</Button> <Button inline size="small" onClick={() => { this.ctrlBodyScroll(false); }}>hidden</Button></div></div>);},ctrlBodyScroll(flag, init) {document.body.style.overflowY = flag ? 'auto' : 'hidden';if (parent && parent !== self && !init) {parent.document.body.style.overflowY = flag ? 'auto' : 'hidden';}},});ReactDOM.render(<Demo />, mountNode);
用于通讯薄等场景
import { ListView, Toast } from 'antd-mobile';const data = [{img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',title: '相约酒店',des: '不是所有的兼职汪都需要风吹日晒',},{img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',title: '麦当劳邀您过周末',des: '不是所有的兼职汪都需要风吹日晒',},{img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',title: '食惠周',des: '不是所有的兼职汪都需要风吹日晒',},];let index = data.length - 1;const NUM_SECTIONS = 5;const NUM_ROWS_PER_SECTION = 5;let pageIndex = 0;const Demo = React.createClass({getInitialState() {const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];const dataSource = new ListView.DataSource({getRowData,getSectionHeaderData: getSectionData,rowHasChanged: (row1, row2) => row1 !== row2,sectionHeaderHasChanged: (s1, s2) => s1 !== s2,});this.dataBlob = {};this.sectionIDs = [];this.rowIDs = [];this.genData = (pIndex = 0) => {for (let i = 0; i < NUM_SECTIONS; i++) {const ii = pIndex * NUM_SECTIONS + i;const sectionName = `Section ${ii}`;this.sectionIDs.push(sectionName);this.dataBlob[sectionName] = sectionName;this.rowIDs[ii] = [];for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {const rowName = `S${ii}, R${jj}`;this.rowIDs[ii].push(rowName);this.dataBlob[rowName] = rowName;}}// new object refthis.sectionIDs = [].concat(this.sectionIDs);this.rowIDs = [].concat(this.rowIDs);};this.genData();return {dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),isLoading: false,};},onEndReached(event) {// load new dataconsole.log('reach end', event);Toast.info('加载新数据');this.setState({ isLoading: true });setTimeout(() => {this.genData(++pageIndex);this.setState({dataSource: this.state.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),isLoading: false,});}, 1000);},render() {const separator = (sectionID, rowID) => (<div key={`${sectionID}-${rowID}`} style={{backgroundColor: '#F5F5F9',height: 8,borderTop: '1px solid #ECECED',borderBottom: '1px solid #ECECED',}}></div>);const row = (rowData, sectionID, rowID) => {if (index < 0) {index = data.length - 1;}const obj = data[index--];return (<div key={rowID}style={{padding: '8px 16px',backgroundColor: 'white',}}><h3 style={{padding: 2,marginBottom: 8,borderBottom: '1px solid #F6F6F6',}}>{obj.title}</h3><div style={{ display: 'flex' }}><img style={{ height: 64, marginRight: 8 }} src={obj.img} /><div><p>{obj.des}</p><p><span style={{ fontSize: 24, color: '#FF6E27' }}>35</span>元/任务</p></div></div></div>);};return (<div><ListViewdataSource={this.state.dataSource}renderHeader={() => <span>header</span>}renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>{this.state.isLoading ? '加载中...' : '加载完毕'}</div>}renderSectionHeader={(sectionData) => (<div>{`任务 ${sectionData.split(' ')[1]}`}</div>)}renderRow={row}renderSeparator={separator}pageSize={4}scrollEventThrottle={20}onScroll={() => { console.log('scroll'); }}onEndReached={this.onEndReached}onEndReachedThreshold={10}stickyHeaderstickyProps={{stickyStyle: { zIndex: 999, top: 43 },topOffset: -43,// isActive: false, // 关闭 sticky 效果}}/></div>);},});ReactDOM.render(<Demo />, mountNode);
用于通讯薄等场景 sticky
import province from 'site/data/province';import { ListView, List, SearchBar } from 'antd-mobile';const { Item } = List;const Demo = React.createClass({getInitialState() {const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];const dataSource = new ListView.DataSource({getRowData,getSectionHeaderData: getSectionData,rowHasChanged: (row1, row2) => row1 !== row2,sectionHeaderHasChanged: (s1, s2) => s1 !== s2,});const dataBlob = {};const sectionIDs = [];const rowIDs = [];Object.keys(province).forEach((item, index) => {sectionIDs.push(item);dataBlob[item] = item;rowIDs[index] = [];province[item].forEach(jj => {rowIDs[index].push(jj.value);dataBlob[jj.value] = jj.label;});});return {dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),headerPressCount: 0,};},render() {return (<div style={{ paddingTop: 40 }}><div style={{ position: 'fixed', zIndex: 999, top: 43, left: 0, right: 0 }}><SearchBarvalue=""placeholder="搜索"onSubmit={(value) => { console.log(`onSubmit${value}`); }}onChange={(value) => { console.log(value); }}onClear={() => { console.log('onClear'); }}onCancel={() => { console.log('onCancel'); }}onFocus={() => { console.log('onFocus'); }}onBlur={() => { console.log('onBlur'); }}/></div><div style={{ position: 'relative' }}><ListView.IndexedListdataSource={this.state.dataSource}renderHeader={() => <span>头部内容请自定义</span>}renderFooter={() => <span>尾部内容请自定义</span>}renderSectionHeader={(sectionData) => (<div>{sectionData}</div>)}renderRow={(rowData) => (<Item>{rowData}</Item>)}style={{ height: 400, overflow: 'auto' }}quickSearchBarStyle={{position: 'absolute',top: 20, right: 10,}}delayTime={10}delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}/></div></div>);},});ReactDOM.render(<Demo />, mountNode);
import province from 'site/data/province';import { ListView, List, SearchBar } from 'antd-mobile';const { Item } = List;const Demo = React.createClass({getInitialState() {const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];const dataSource = new ListView.DataSource({getRowData,getSectionHeaderData: getSectionData,rowHasChanged: (row1, row2) => row1 !== row2,sectionHeaderHasChanged: (s1, s2) => s1 !== s2,});const dataBlob = {};const sectionIDs = [];const rowIDs = [];Object.keys(province).forEach((item, index) => {sectionIDs.push(item);dataBlob[item] = item;rowIDs[index] = [];province[item].forEach(jj => {rowIDs[index].push(jj.value);dataBlob[jj.value] = jj.label;});});return {dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),headerPressCount: 0,};},render() {return (<div style={{ paddingTop: 40 }}><div style={{ position: 'fixed', zIndex: 999, top: 43, left: 0, right: 0 }}><SearchBarvalue=""placeholder="搜索"onSubmit={(value) => { console.log(`onSubmit${value}`); }}onChange={(value) => { console.log(value); }}onClear={() => { console.log('onClear'); }}onCancel={() => { console.log('onCancel'); }}onFocus={() => { console.log('onFocus'); }}onBlur={() => { console.log('onBlur'); }}/></div><ListView.IndexedListdataSource={this.state.dataSource}renderHeader={() => <span>头部内容请自定义</span>}renderFooter={() => <span>尾部内容请自定义</span>}renderSectionHeader={(sectionData) => (<div>{sectionData}</div>)}renderRow={(rowData) => (<Item>{rowData}</Item>)}stickyHeaderstickyProps={{stickyStyle: { zIndex: 999, top: 83 },topOffset: -83,}}quickSearchBarStyle={{top: 85,}}delayTime={10}delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}/></div>);},});ReactDOM.render(<Demo />, mountNode);

API
same as React Native ListView(v0.26).不支持的特性
一般情况下,不支持“平台特有”的API,例如
androidendFillColor、iosalwaysBounceHorizontal。另外,使用 css 代替 react-native 的 style 设置方式。
onChangeVisibleRows
stickyHeaderIndices
ScrollView props:
keyboardDismissMode (not support control keyboard)
keyboardShouldPersistTaps (not support control keyboard)
onContentSizeChange (use onLayout instead)
removeClippedSubviews
showsHorizontalScrollIndicator (use css style instead)
showsVerticalScrollIndicator (use css style instead)
View props: 注意:只支持
onLayoutprop
新增API
stickyHeader 固定区块标题到页面顶部 (注意: 设置后,ScrollComponent 将被渲染到 body 的第一个元素里)
- stickyProps / stickyContainerProps (see react-sticky)
renderBodyComponent 渲染自定义的 body 组件
ListView.IndexedList (beta)
支持右侧导航功能注意:由于需要直接scroll到任意位置、只支持分两步渲染,所以列表数据量过大时、性能会有影响
quickSearchBarTop (object{value:string, label:string}, 默认为'#') - 快捷导航栏置顶按钮
quickSearchBarStyle (object) - quickSearchBar 的 style
onQuickSearch (function()) 快捷导航切换时触发
delayTime (number) - 默认 100ms, 延迟渲染时间设置(用于首屏优化,一开始渲染
initialListSize数量的数据,在此时间后、延迟渲染剩余的数据项、即totalRowCount - initialListSize)delayActivityIndicator (react node) - 延迟渲染的 loading 指示器