ListView 长列表
适用于显示同类的长列表数据类型,对渲染性能有一定的优化效果。代码演示
Note: you need to set
height
/overflow
style.
use html
/* eslint no-dupe-keys: 0, no-mixed-operators: 0 */
import { ListView } from 'antd-mobile';
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 data = [
{
img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
title: 'Meet hotel',
des: '不是所有的兼职汪都需要风吹日晒',
},
{
img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
title: 'McDonald\'s invites you',
des: '不是所有的兼职汪都需要风吹日晒',
},
{
img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
title: 'Eat the week',
des: '不是所有的兼职汪都需要风吹日晒',
},
];
const NUM_SECTIONS = 5;
const NUM_ROWS_PER_SECTION = 5;
let pageIndex = 0;
const dataBlobs = {};
let sectionIDs = [];
let rowIDs = [];
function genData(pIndex = 0) {
for (let i = 0; i < NUM_SECTIONS; i++) {
const ii = (pIndex * NUM_SECTIONS) + i;
const sectionName = `Section ${ii}`;
sectionIDs.push(sectionName);
dataBlobs[sectionName] = sectionName;
rowIDs[ii] = [];
for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
const rowName = `S${ii}, R${jj}`;
rowIDs[ii].push(rowName);
dataBlobs[rowName] = rowName;
}
}
sectionIDs = [...sectionIDs];
rowIDs = [...rowIDs];
}
class Demo extends React.Component {
constructor(props) {
super(props);
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.state = {
dataSource,
isLoading: true,
height: document.documentElement.clientHeight * 3 / 4,
};
}
componentDidMount() {
// you can scroll to the specified position
// setTimeout(() => this.lv.scrollTo(0, 120), 800);
const hei = document.documentElement.clientHeight - ReactDOM.findDOMNode(this.lv).parentNode.offsetTop;
// simulate initial Ajax
setTimeout(() => {
genData();
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlobs, sectionIDs, rowIDs),
isLoading: false,
height: hei,
});
}, 600);
}
// If you use redux, the data maybe at props, you need use `componentWillReceiveProps`
// componentWillReceiveProps(nextProps) {
// if (nextProps.dataSource !== this.props.dataSource) {
// this.setState({
// dataSource: this.state.dataSource.cloneWithRowsAndSections(nextProps.dataSource),
// });
// }
// }
onEndReached = (event) => {
// load new data
// hasMore: from backend data, indicates whether it is the last page, here is false
if (this.state.isLoading && !this.state.hasMore) {
return;
}
console.log('reach end', event);
this.setState({ isLoading: true });
setTimeout(() => {
genData(++pageIndex);
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlobs, sectionIDs, 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',
}}
/>
);
let index = data.length - 1;
const row = (rowData, sectionID, rowID) => {
if (index < 0) {
index = data.length - 1;
}
const obj = data[index--];
return (
<div key={rowID} style={{ padding: '0 15px' }}>
<div
style={{
lineHeight: '50px',
color: '#888',
fontSize: 18,
borderBottom: '1px solid #F6F6F6',
}}
>{obj.title}</div>
<div style={{ display: '-webkit-box', display: 'flex', padding: '15px 0' }}>
<img style={{ height: '64px', marginRight: '15px' }} src={obj.img} alt="" />
<div style={{ lineHeight: 1 }}>
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>{obj.des}</div>
<div><span style={{ fontSize: '30px', color: '#FF6E27' }}>35</span>¥ {rowID}</div>
</div>
</div>
</div>
);
};
return (
<ListView
ref={el => this.lv = el}
dataSource={this.state.dataSource}
renderHeader={() => <span>header</span>}
renderFooter={() => (<div style={{ padding: 30, textAlign: 'center' }}>
{this.state.isLoading ? 'Loading...' : 'Loaded'}
</div>)}
renderSectionHeader={sectionData => (
<div>{`Task ${sectionData.split(' ')[1]}`}</div>
)}
renderBodyComponent={() => <MyBody />}
renderRow={row}
renderSeparator={separator}
style={{
height: this.state.height,
overflow: 'auto',
}}
pageSize={4}
onScroll={() => { console.log('scroll'); }}
scrollRenderAheadDistance={500}
onEndReached={this.onEndReached}
onEndReachedThreshold={10}
/>
);
}
}
ReactDOM.render(<Demo />, mountNode);
body
as a scroll container.
sticky block header to the top of the page
/* eslint no-dupe-keys: 0 */
import { ListView } from 'antd-mobile';
const data = [
{
img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
title: 'Meet hotel',
des: '不是所有的兼职汪都需要风吹日晒',
},
{
img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
title: 'McDonald\'s invites you',
des: '不是所有的兼职汪都需要风吹日晒',
},
{
img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
title: 'Eat the week',
des: '不是所有的兼职汪都需要风吹日晒',
},
];
const NUM_ROWS = 20;
let pageIndex = 0;
function 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;
}
class Demo extends React.Component {
constructor(props) {
super(props);
const dataSource = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
});
this.state = {
dataSource,
isLoading: true,
};
}
componentDidMount() {
// you can scroll to the specified position
// setTimeout(() => this.lv.scrollTo(0, 120), 800);
// simulate initial Ajax
setTimeout(() => {
this.rData = genData();
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.rData),
isLoading: false,
});
}, 600);
}
// If you use redux, the data maybe at props, you need use `componentWillReceiveProps`
// componentWillReceiveProps(nextProps) {
// if (nextProps.dataSource !== this.props.dataSource) {
// this.setState({
// dataSource: this.state.dataSource.cloneWithRows(nextProps.dataSource),
// });
// }
// }
onEndReached = (event) => {
// load new data
// hasMore: from backend data, indicates whether it is the last page, here is false
if (this.state.isLoading && !this.state.hasMore) {
return;
}
console.log('reach end', event);
this.setState({ isLoading: true });
setTimeout(() => {
this.rData = { ...this.rData, ...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',
}}
/>
);
let index = data.length - 1;
const row = (rowData, sectionID, rowID) => {
if (index < 0) {
index = data.length - 1;
}
const obj = data[index--];
return (
<div key={rowID} style={{ padding: '0 15px' }}>
<div
style={{
lineHeight: '50px',
color: '#888',
fontSize: 18,
borderBottom: '1px solid #F6F6F6',
}}
>{obj.title}</div>
<div style={{ display: '-webkit-box', display: 'flex', padding: '15px 0' }}>
<img style={{ height: '64px', marginRight: '15px' }} src={obj.img} alt="" />
<div style={{ lineHeight: 1 }}>
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>{obj.des}</div>
<div><span style={{ fontSize: '30px', color: '#FF6E27' }}>{rowID}</span>¥</div>
</div>
</div>
</div>
);
};
return (
<ListView
ref={el => this.lv = el}
dataSource={this.state.dataSource}
renderHeader={() => <span>header</span>}
renderFooter={() => (<div style={{ padding: 30, textAlign: 'center' }}>
{this.state.isLoading ? 'Loading...' : 'Loaded'}
</div>)}
renderRow={row}
renderSeparator={separator}
className="am-list"
pageSize={4}
useBodyScroll
onScroll={() => { console.log('scroll'); }}
scrollRenderAheadDistance={500}
onEndReached={this.onEndReached}
onEndReachedThreshold={10}
/>
);
}
}
ReactDOM.render(<Demo />, mountNode);
/* eslint no-dupe-keys: 0 */
import { ListView } from 'antd-mobile';
import { StickyContainer, Sticky } from 'react-sticky';
const data = [
{
img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
title: 'Meet hotel',
des: '不是所有的兼职汪都需要风吹日晒',
},
{
img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
title: 'McDonald\'s invites you',
des: '不是所有的兼职汪都需要风吹日晒',
},
{
img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
title: 'Eat the week',
des: '不是所有的兼职汪都需要风吹日晒',
},
];
const NUM_SECTIONS = 5;
const NUM_ROWS_PER_SECTION = 5;
let pageIndex = 0;
const dataBlobs = {};
let sectionIDs = [];
let rowIDs = [];
function genData(pIndex = 0) {
for (let i = 0; i < NUM_SECTIONS; i++) {
const ii = (pIndex * NUM_SECTIONS) + i;
const sectionName = `Section ${ii}`;
sectionIDs.push(sectionName);
dataBlobs[sectionName] = sectionName;
rowIDs[ii] = [];
for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
const rowName = `S${ii}, R${jj}`;
rowIDs[ii].push(rowName);
dataBlobs[rowName] = rowName;
}
}
sectionIDs = [...sectionIDs];
rowIDs = [...rowIDs];
}
class Demo extends React.Component {
constructor(props) {
super(props);
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.state = {
dataSource,
isLoading: true,
};
}
componentDidMount() {
// you can scroll to the specified position
// setTimeout(() => this.lv.scrollTo(0, 120), 800);
// simulate initial Ajax
setTimeout(() => {
genData();
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlobs, sectionIDs, rowIDs),
isLoading: false,
});
}, 600);
}
// If you use redux, the data maybe at props, you need use `componentWillReceiveProps`
// componentWillReceiveProps(nextProps) {
// if (nextProps.dataSource !== this.props.dataSource) {
// this.setState({
// dataSource: this.state.dataSource.cloneWithRowsAndSections(nextProps.dataSource),
// });
// }
// }
onEndReached = (event) => {
// load new data
// hasMore: from backend data, indicates whether it is the last page, here is false
if (this.state.isLoading && !this.state.hasMore) {
return;
}
console.log('reach end', event);
this.setState({ isLoading: true });
setTimeout(() => {
genData(++pageIndex);
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlobs, sectionIDs, 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',
}}
/>
);
let index = data.length - 1;
const row = (rowData, sectionID, rowID) => {
if (index < 0) {
index = data.length - 1;
}
const obj = data[index--];
return (
<div key={rowID} style={{ padding: '0 15px' }}>
<div
style={{
lineHeight: '50px',
color: '#888',
fontSize: 18,
borderBottom: '1px solid #F6F6F6',
}}
>{obj.title}</div>
<div style={{ display: '-webkit-box', display: 'flex', padding: '15px 0' }}>
<img style={{ height: '64px', marginRight: '15px' }} src={obj.img} alt="" />
<div style={{ lineHeight: 1 }}>
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>{obj.des}</div>
<div><span style={{ fontSize: '30px', color: '#FF6E27' }}>35</span>¥ {rowID}</div>
</div>
</div>
</div>
);
};
return (
<ListView
ref={el => this.lv = el}
dataSource={this.state.dataSource}
className="am-list sticky-list"
useBodyScroll
renderSectionWrapper={sectionID => (
<StickyContainer
key={`s_${sectionID}_c`}
className="sticky-container"
style={{ zIndex: 4 }}
/>
)}
renderSectionHeader={sectionData => (
<Sticky>
{({
style,
}) => (
<div
className="sticky"
style={{
...style,
zIndex: 3,
backgroundColor: parseInt(sectionData.replace('Section ', ''), 10) % 2 ?
'#5890ff' : '#F8591A',
color: 'white',
}}
>{`Task ${sectionData.split(' ')[1]}`}</div>
)}
</Sticky>
)}
renderHeader={() => <span>header</span>}
renderFooter={() => (<div style={{ padding: 30, textAlign: 'center' }}>
{this.state.isLoading ? 'Loading...' : 'Loaded'}
</div>)}
renderRow={row}
renderSeparator={separator}
pageSize={4}
onScroll={() => { console.log('scroll'); }}
scrollEventThrottle={200}
onEndReached={this.onEndReached}
onEndReachedThreshold={10}
/>
);
}
}
ReactDOM.render(<Demo />, mountNode);
sticky index List
.sticky-list .sticky-container .am-list-item { padding-left: 0; }
.sticky-list .sticky-container .am-list-line { padding-right: 0; }
.sticky-list .sticky-container .am-list-line .am-list-content { padding-top: 0; padding-bottom: 0; }
.sticky-list .sticky-container .sticky { padding: 7px 15px; transform: none; }
import { province } from 'antd-mobile-demo-data';
import { StickyContainer, Sticky } from 'react-sticky';
import { ListView, List, SearchBar } from 'antd-mobile';
const { Item } = List;
function genData(ds, provinceData) {
const dataBlob = {};
const sectionIDs = [];
const rowIDs = [];
Object.keys(provinceData).forEach((item, index) => {
sectionIDs.push(item);
dataBlob[item] = item;
rowIDs[index] = [];
provinceData[item].forEach((jj) => {
rowIDs[index].push(jj.value);
dataBlob[jj.value] = jj.label;
});
});
return ds.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs);
}
class Demo extends React.Component {
constructor(props) {
super(props);
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.state = {
inputValue: '',
dataSource,
isLoading: true,
};
}
componentDidMount() {
// simulate initial Ajax
setTimeout(() => {
this.setState({
dataSource: genData(this.state.dataSource, province),
isLoading: false,
});
}, 600);
}
onSearch = (val) => {
const pd = { ...province };
Object.keys(pd).forEach((item) => {
const arr = pd[item].filter(jj => jj.spell.toLocaleLowerCase().indexOf(val) > -1);
if (!arr.length) {
delete pd[item];
} else {
pd[item] = arr;
}
});
this.setState({
inputValue: val,
dataSource: genData(this.state.dataSource, pd),
});
}
render() {
return (<div style={{ paddingTop: '44px', position: 'relative' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
<SearchBar
value={this.state.inputValue}
placeholder="Search"
onChange={this.onSearch}
onClear={() => { console.log('onClear'); }}
onCancel={() => { console.log('onCancel'); }}
/>
</div>
<ListView.IndexedList
dataSource={this.state.dataSource}
className="am-list sticky-list"
useBodyScroll
renderSectionWrapper={sectionID => (
<StickyContainer
key={`s_${sectionID}_c`}
className="sticky-container"
style={{ zIndex: 4 }}
/>
)}
renderSectionHeader={sectionData => (
<Sticky>
{({
style,
}) => (
<div
className="sticky"
style={{
...style,
zIndex: 3,
backgroundColor: sectionData.charCodeAt(0) % 2 ? '#5890ff' : '#F8591A',
color: 'white',
}}
>{sectionData}</div>
)}
</Sticky>
)}
renderHeader={() => <span>custom header</span>}
renderFooter={() => <span>custom footer</span>}
renderRow={rowData => (<Item>{rowData}</Item>)}
quickSearchBarStyle={{
top: 85,
}}
delayTime={10}
delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>rendering...</div>}
/>
</div>);
}
}
ReactDOM.render(<Demo />, mountNode);
.sticky-list .sticky-container .am-list-item { padding-left: 0; }
.sticky-list .sticky-container .am-list-line { padding-right: 0; }
.sticky-list .sticky-container .am-list-line .am-list-content { padding-top: 0; padding-bottom: 0; }
.sticky-list .sticky-container .sticky { padding: 7px 15px; transform: none; }
API
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
dataSource | ListView.DataSource 实例 | ListViewDataSource | - |
initialListSize | 指定在组件刚挂载的时候渲染多少行数据,用这个属性来确保首屏显示合适数量的数据 | number | - |
onEndReached | 当所有的数据都已经渲染过,并且列表被滚动到距离最底部不足onEndReachedThreshold 个像素的距离时调用 | (event?) => {} | - |
onEndReachedThreshold | 调用onEndReached 之前的临界值,单位是像素 | number | 1000 |
pageSize | 每次事件循环(每帧)渲染的行数 | number | 1 |
renderHeader / renderFooter | 页头与页脚(如果提供)会在每次渲染过程中都重新渲染。如果它们重绘的性能开销很大,把他们包装到一个StaticContainer或者其它恰当的结构中。页脚在列表的最底部,而页头会在最顶部 | () => renderable | - |
renderRow | 从数据源(data source)中接受一条数据,以及它和它所在section的ID。返回一个可渲染的组件来为这行数据进行渲染。默认情况下参数中的数据就是放进数据源中的数据本身,不过也可以提供一些转换器。如果某一行正在被高亮(通过调用highlightRow函数),ListView会得到相应的通知。 | (rowData, sectionID, rowID, highlightRow) => renderable | - |
renderScrollComponent | 指定一个函数,在其中返回一个可以滚动的组件,ListView将会在该组件内部进行渲染。默认情况下会返回一个包含指定属性的ScrollView。 | (props) => renderable | - |
renderSectionHeader | 如果提供了此函数,会为每个小节(section)渲染一个标题 | (sectionData, sectionID) => renderable | - |
renderSeparator | 如果提供了此属性,一个可渲染的组件会被渲染在每一行下面,除了小节标题的前面的最后一行。在其上方的小节ID和行ID,以及邻近的行是否被高亮会作为参数传递进来。 | (sectionID, rowID, adjacentRowHighlighted) => renderable | - |
scrollRenderAheadDistance | 当一个行接近屏幕范围多少像素之内的时候,就开始渲染这一行 | number | 1000 |
contentContainerStyle | 这些样式会应用到一个内层的内容容器上,所有的子视图都会包裹在内容容器内 | Object | - |
horizontal | 当此属性为true的时候,所有的的子视图会在水平方向上排成一行,而不是默认的在垂直方向上排成一列 | bool | false |
onContentSizeChange | 此函数会在 ScrollView 内部可滚动内容的视图发生变化时调用。 | (contentWidth, contentHeight) => {} | - |
onScroll | 在滚动的过程中,每帧最多调用一次此回调函数。调用的频率可以用scrollEventThrottle 属性来控制。 | e => {} | - |
scrollEventThrottle | 控制在滚动过程中,scroll事件被调用的频率 | number | 50 |
onLayout | 当组件挂载或者布局变化的时候调用 | ({nativeEvent:{ layout:{ width, height }}}) => {} | - |
—— | |||
renderBodyComponent | 自定义 body 的包裹组件 | () => renderable | - |
renderSectionWrapper | 渲染自定义的区块包裹组件 | (sectionID) => renderable | - |
renderSectionBodyWrapper | 渲染自定义的区块 body 包裹组件 | (sectionID) => renderable | - |
useBodyScroll | 使用 html 的 body 作为滚动容器 | bool | false |
pullToRefresh | 使用 pullToRefresh, 你需要和 PullToRefresh 组件一起使用 | bool | false |
方法
getMetrics() - 导出一些用于性能分析的数据。
scrollTo(…args) - 滚动到指定的x, y偏移处(暂不支持过渡动画)。
ListView.IndexedList
此组件常用于 “通讯录”/“城市列表” 等场景中,支持索引导航功能。你可以使用 ListView 上的几乎所有 APIs。
注意:由于索引列表可以点击任一项索引来定位其内容、即内容需要直接滚动到任意位置,这样就难以做到像 ListView 一样能在滚动时自动懒渲染。目前实现上只支持分两步渲染,能借此达到首屏优先显示目的,但如果列表数据量过大时、整体性能仍会有影响。
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
quickSearchBarTop | 快捷导航栏最顶部按钮、常用于回到顶部 | object{value:string, label:string} | { value: '#', label: '#' } |
quickSearchBarStyle | quickSearchBar 的 style | object | - |
onQuickSearch | 快捷导航切换时调用 | (sectionID: any, topId?:any) => void | - |
showQuickSearchIndicator | whether show quick search indicator | bool | false |
delayTime | 延迟渲染时间设置(用于首屏优化,一开始渲染initialListSize 数量的数据,在此时间后、延迟渲染剩余的数据项、即totalRowCount - initialListSize ) | number | 100ms |
delayActivityIndicator | 延迟渲染的 loading 指示器 | react node | - |
提示
ListView 有两种类型的滚动容器:局部 div 容器
- 默认,注意:需要手动给 ListView 设置高度
html 的 body 容器
- 设置
useBodyScroll
后生效 (不需要设置高度)
- 设置
this._renderMoreRowsIfNeeded()
,由于此时this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()
即已经渲染的数据与 dataSource 里已有的数据项个数相同,所以 ListView 认为应该再调用 onEndReached 方法。
onEndReached 为什么会不停调用?520#issuecomment-263510596其他问题:#633#573#541