ListView 长列表
最适用于显示同类的长列表数据类型,对渲染性能有一定的优化效果代码演示
注意:需要设置 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;
function MyBody(props) {
return (
<div className="am-list-body my-body">
<span style={{ display: 'none' }}>you can custom body wrap element</span>
{props.children}
</div>
);
}
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,
};
},
componentDidMount() {
// you can scroll to the specified position
// this.refs.lv.refs.listview.scrollTo(0, 200);
},
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: '0.08rem 0.16rem',
backgroundColor: 'white',
}}
>
<h3 style={{ padding: 2, marginBottom: '0.08rem', borderBottom: '1px solid #F6F6F6' }}>
{obj.title}
</h3>
<div style={{ display: '-webkit-box', display: 'flex' }}>
<img style={{ height: '1.28rem', marginRight: '0.08rem' }} 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 ref="lv"
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>
)}
renderBodyComponent={() => <MyBody />}
renderRow={row}
renderSeparator={separator}
className="fortest"
style={{
height: document.body.clientHeight * 3 / 4,
overflow: 'auto',
border: '1px solid #ddd',
margin: '0.1rem 0',
}}
pageSize={4}
scrollRenderAheadDistance={500}
scrollEventThrottle={20}
onScroll={() => { console.log('scroll'); }}
onEndReached={this.onEndReached}
onEndReachedThreshold={10}
/>
</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 = this.genData();
return {
dataSource: dataSource.cloneWithRows(this.rData),
isLoading: false,
};
},
componentDidMount() {
// you can scroll to the specified position
// this.refs.lv.refs.listview.scrollTo(0, 200);
},
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: '0.08rem 0.16rem',
backgroundColor: 'white',
}}
>
<h3 style={{ padding: 2, marginBottom: '0.08rem', borderBottom: '1px solid #F6F6F6' }}>
{obj.title}
</h3>
<div style={{ display: '-webkit-box', display: 'flex' }}>
<img style={{ height: '1.28rem', marginRight: '0.08rem' }} src={obj.img} />
<div style={{ display: 'inline-block' }}>
<p>{obj.des}</p>
<p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>{rowID}</span>元/任务</p>
</div>
</div>
</div>
);
};
return (
<ListView ref="lv"
dataSource={this.state.dataSource}
renderHeader={() => <span>header</span>}
renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
{this.state.isLoading ? '加载中...' : '加载完毕'}
</div>}
renderRow={row}
renderSeparator={separator}
className="am-list"
pageSize={4}
scrollRenderAheadDistance={500}
scrollEventThrottle={20}
onScroll={() => { console.log('scroll'); }}
useBodyScroll
onEndReached={this.onEndReached}
onEndReachedThreshold={10}
/>
);
},
});
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: '0.08rem 0.16rem',
backgroundColor: 'white',
}}
>
<h3 style={{ padding: 2, marginBottom: '0.08rem', borderBottom: '1px solid #F6F6F6' }}>
{obj.title}
</h3>
<div style={{ display: '-webkit-box', display: 'flex' }}>
<img style={{ height: '1.28rem', marginRight: '0.08rem' }} 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 (
<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}
className="am-list"
pageSize={4}
scrollEventThrottle={20}
onScroll={() => { console.log('scroll'); }}
onEndReached={this.onEndReached}
onEndReachedThreshold={10}
stickyHeader
stickyProps={{
stickyStyle: { zIndex: 999, WebkitTransform: 'none', transform: 'none' },
// topOffset: -43,
// isActive: false, // 关闭 sticky 效果
}}
stickyContainerProps={{
className: 'for-stickyContainer-demo',
}}
/>
);
},
});
ReactDOM.render(<Demo />, mountNode);
用于通讯薄等场景 “吸顶”(sticky)
/* eslint no-mixed-operators: 0 */
import { province } from 'antd-mobile-demo-data';
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 (
<ListView.IndexedList
dataSource={this.state.dataSource}
renderHeader={() => <span>头部内容请自定义</span>}
renderFooter={() => <span>尾部内容请自定义</span>}
renderSectionHeader={(sectionData) => (<div className="ih">{sectionData}</div>)}
renderRow={(rowData) => (<Item>{rowData}</Item>)}
className="fortest"
style={{
height: document.body.clientHeight * 3 / 4,
overflow: 'auto',
}}
quickSearchBarStyle={{
position: 'absolute',
top: 20,
}}
delayTime={10}
delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
/>
);
},
});
ReactDOM.render(<Demo />, mountNode);
import { province as provinceData } from 'antd-mobile-demo-data';
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: '0.88rem', position: 'relative' }}>
<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 className="ih">{sectionData}</div>)}
renderRow={(rowData) => (<Item>{rowData}</Item>)}
className="am-list"
stickyHeader
stickyProps={{
stickyStyle: { zIndex: 999 },
}}
quickSearchBarStyle={{
top: 85,
}}
delayTime={10}
delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
/>
</div>);
},
});
ReactDOM.render(<Demo />, mountNode);
API ( 适用平台:WEB、React-Native )
React-Native 平台直接使用 React Native ListView
WEB 平台使用 React Native ListView(v0.26) 的 API,但有一些差异,以下列出差异详情
React Native ListView 在 WEB 平台上不被支持的 API 列表:
一般情况下,不支持“平台特有”的API,例如
android
endFillColor、iOS
alwaysBounceHorizontal。另外,使用 css 代替 react-native 的 style 设置方式。
onChangeVisibleRows
stickyHeaderIndices
ScrollView 组件中不被支持的 API:
keyboardDismissMode
keyboardShouldPersistTaps
onContentSizeChange (可使用
onLayout
替代)removeClippedSubviews
scrollEnabled
showsHorizontalScrollIndicator (可使用 css style 替代)
showsVerticalScrollIndicator (可使用 css style 替代)
View 组件 API: 只支持
onLayout
WEB 平台新增API
useBodyScroll (boolean, false) - 使用 html 的
body
作为滚动容器stickyHeader (boolean, false) - 固定区块标题到页面顶部 (注意: 设置后会自动使用 html 的
body
作为滚动容器)- 开启 sticky 后还可以设置 stickyProps / stickyContainerProps (详见 react-sticky)
renderBodyComponent (function, () => React.Element) - 自定义 body 的包裹组件
renderSectionBodyWrapper (function, (sectionID: any) => React.Element) - 渲染自定义的区块包裹组件
useZscroller (boolean, false) - 使用 zscroller 来模拟实现滚动容器 (可用于一些低端 Android 机上)
- 注意:开启后
useBodyScroll
和stickyHeader
设置会自动被忽略
- 注意:开启后
scrollerOptions - 详见 zscroller options
WEB 平台新增 ListView.IndexedList 组件
此组件常用于 “通讯录”/“城市列表” 等场景中,支持索引导航功能。注意:由于索引列表可以点击任一项索引来定位其内容、即内容需要直接滚动到任意位置,这样就难以做到像 ListView 一样能在滚动时自动懒渲染。目前实现上只支持分两步渲染,能借此达到首屏优先显示目的,但如果列表数据量过大时、整体性能仍会有影响。
quickSearchBarTop (object{value:string, label:string}, value/label 默认为'#') - 快捷导航栏最顶部按钮、常用于回到顶部
quickSearchBarStyle (object) - quickSearchBar 的 style
onQuickSearch (function, (sectionID: any, topId?:any) => void) 快捷导航切换时调用
delayTime (number) - 默认 100ms, 延迟渲染时间设置(用于首屏优化,一开始渲染
initialListSize
数量的数据,在此时间后、延迟渲染剩余的数据项、即totalRowCount - initialListSize
)delayActivityIndicator (react node) - 延迟渲染的 loading 指示器
常见问题与实现原理
onEndReached 为什么会不停调用? https://github.com/ant-design/ant-design-mobile/issues/520#issuecomment-263510596
如何设置滚动到列表的某一位置?(例如,点击列表某一项进入另一个页面,再返回到原位置) #541
其他问题:#633 #573
html 的 body 容器
局部 div 容器 (通过 ref 获取到)
使用 zscroller 的模拟滚动容器
this._renderMoreRowsIfNeeded()
,由于此时this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()
即已经渲染的数据与 dataSource 里已有的数据项个数相同,所以 ListView 认为应该再调用 onEndReached 方法。