ListView 长列表
单个连续模块垂直排列,显示多行文本内容。eg:城市选择。规则
最适用于显示同类的数据类型或者数据类型组。
只支持垂直滚动。
可以通过日期、字母顺序或者其它参数来过滤数据或改变每行列表的顺序。
代码演示
注意:需要设置 ListView 的 style 的
height
/overflow
,以此作为滚动容器。同时建议设置
body
的overflow: hidden
使用 html 的
/* eslint no-dupe-keys: 0, no-mixed-operators: 0 */
import { ListView } 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);
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',
}} />
);
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: '-webkit-box', display: 'flex' }}>
<img style={{ height: 64 * (window.viewportScale || 1), marginRight: 8 }} src={obj.img} />
<div style={{ display: 'inline-block' }}>
<p>{obj.des}</p>
<p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>35</span>元/任务</p>
</div>
</div>
</div>
);
};
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: document.body.clientHeight * 3 / 4,
overflow: 'auto',
border: '1px solid #ddd',
margin: '10px 0',
}}
/>
</div>);
},
});
ReactDOM.render(<Demo />, mountNode);
body
作为滚动容器
区块标题 “吸顶”(sticky) 功能示例
/* eslint no-dupe-keys: 0 */
import { ListView } 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_ROWS = 20;
let pageIndex = 0;
const Demo = React.createClass({
getInitialState() {
const dataSource = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
});
this.genData = (pIndex = 0) => {
const dataBlob = {};
for (let i = 0; i < NUM_ROWS; i++) {
const ii = (pIndex * NUM_ROWS) + i;
dataBlob[`${ii}`] = `row - ${ii}`;
}
return dataBlob;
};
this.rData = {};
return {
dataSource: dataSource.cloneWithRows(this.genData()),
isLoading: false,
};
},
onEndReached(event) {
// load new data
console.log('reach end', event);
this.setState({ isLoading: true });
setTimeout(() => {
this.rData = { ...this.rData, ...this.genData(++pageIndex) };
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.rData),
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',
}} />
);
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: '-webkit-box', display: 'flex' }}>
<img style={{ height: 64 * (window.viewportScale || 1), marginRight: 8 }} src={obj.img} />
<div style={{ display: 'inline-block' }}>
<p>{obj.des}</p>
<p><span style={{ fontSize: '1.6em', 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>}
renderRow={row}
renderSeparator={separator}
pageSize={4}
scrollRenderAheadDistance={500}
scrollEventThrottle={20}
onScroll={() => { console.log('scroll'); }}
useBodyScroll
onEndReached={this.onEndReached}
onEndReachedThreshold={10}
/>
</div>);
},
});
ReactDOM.render(<Demo />, mountNode);
用于通讯薄等场景
/* eslint no-dupe-keys: 0 */
import { ListView } 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);
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',
}} />
);
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: '-webkit-box', display: 'flex' }}>
<img style={{ height: 64 * (window.viewportScale || 1), marginRight: 8 }} src={obj.img} />
<div style={{ display: 'inline-block' }}>
<p>{obj.des}</p>
<p><span style={{ fontSize: '1.6em', 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 },
// topOffset: -43,
// isActive: false, // 关闭 sticky 效果
}}
/>
</div>);
},
});
ReactDOM.render(<Demo />, mountNode);
用于通讯薄等场景 “吸顶”(sticky)
/* eslint no-mixed-operators: 0 */
import province from 'site/data/province';
import { ListView, List } 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>
<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: document.body.clientHeight * 3 / 4,
overflow: 'auto',
}}
quickSearchBarStyle={{
position: 'absolute',
top: 20,
}}
delayTime={10}
delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
/>
</div>
</div>);
},
});
ReactDOM.render(<Demo />, mountNode);
import provinceData 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,
});
this.createDs = (ds, province) => {
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 ds.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs);
};
return {
inputValue: '',
dataSource: this.createDs(dataSource, provinceData),
headerPressCount: 0,
};
},
onSearch(val) {
const pd = { ...provinceData };
Object.keys(pd).forEach((item) => {
pd[item] = pd[item].filter(jj => jj.spell.toLocaleLowerCase().indexOf(val) > -1);
});
this.setState({
inputValue: val,
dataSource: this.createDs(this.state.dataSource, pd),
});
},
render() {
return (<div style={{ paddingTop: 44 * (window.viewportScale || 1) }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
<SearchBar
value={this.state.inputValue}
placeholder="搜索"
onChange={this.onSearch}
onClear={() => { console.log('onClear'); }}
onCancel={() => { console.log('onCancel'); }}
/>
</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 },
}}
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 (web)
useBodyScroll (boolean, false) - 使用 html 的
body
作为滚动容器stickyHeader 固定区块标题到页面顶部 (注意: 设置后,ScrollComponent 将被渲染到 body 的第一个元素里,使用 html 的
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 指示器