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 ref
this.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 data
console.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%' }}>
<ListView
dataSource={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 ref
this.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 data
console.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>
<ListView
dataSource={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}
stickyHeader
stickyProps={{
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 }}>
<SearchBar
value=""
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.IndexedList
dataSource={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 }}>
<SearchBar
value=""
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.IndexedList
dataSource={this.state.dataSource}
renderHeader={() => <span>头部内容请自定义</span>}
renderFooter={() => <span>尾部内容请自定义</span>}
renderSectionHeader={(sectionData) => (<div>{sectionData}</div>)}
renderRow={(rowData) => (<Item>{rowData}</Item>)}
stickyHeader
stickyProps={{
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,例如
android
endFillColor、ios
alwaysBounceHorizontal。另外,使用 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: 注意:只支持
onLayout
prop
新增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 指示器