ListView 长列表

单个连续模块垂直排列,显示多行文本内容。eg:城市选择。

规则

  • 最适用于显示同类的数据类型或者数据类型组。

  • 只支持垂直滚动。

  • 可以通过日期、字母顺序或者其它参数来过滤数据或改变每行列表的顺序。

代码演示

子容器

注意:需要设置 ListView 的 style 的 height/overflow,以此作为滚动容器。

同时建议设置bodyoverflow: hidden

  1. /* eslint no-dupe-keys: 0, no-mixed-operators: 0 */
  2. import { ListView } from 'antd-mobile';
  3. const data = [
  4. {
  5. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  6. title: '相约酒店',
  7. des: '不是所有的兼职汪都需要风吹日晒',
  8. },
  9. {
  10. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  11. title: '麦当劳邀您过周末',
  12. des: '不是所有的兼职汪都需要风吹日晒',
  13. },
  14. {
  15. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  16. title: '食惠周',
  17. des: '不是所有的兼职汪都需要风吹日晒',
  18. },
  19. ];
  20. let index = data.length - 1;
  21. const NUM_SECTIONS = 5;
  22. const NUM_ROWS_PER_SECTION = 5;
  23. let pageIndex = 0;
  24. const Demo = React.createClass({
  25. getInitialState() {
  26. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  27. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  28. const dataSource = new ListView.DataSource({
  29. getRowData,
  30. getSectionHeaderData: getSectionData,
  31. rowHasChanged: (row1, row2) => row1 !== row2,
  32. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  33. });
  34. this.dataBlob = {};
  35. this.sectionIDs = [];
  36. this.rowIDs = [];
  37. this.genData = (pIndex = 0) => {
  38. for (let i = 0; i < NUM_SECTIONS; i++) {
  39. const ii = (pIndex * NUM_SECTIONS) + i;
  40. const sectionName = `Section ${ii}`;
  41. this.sectionIDs.push(sectionName);
  42. this.dataBlob[sectionName] = sectionName;
  43. this.rowIDs[ii] = [];
  44. for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
  45. const rowName = `S${ii}, R${jj}`;
  46. this.rowIDs[ii].push(rowName);
  47. this.dataBlob[rowName] = rowName;
  48. }
  49. }
  50. // new object ref
  51. this.sectionIDs = [].concat(this.sectionIDs);
  52. this.rowIDs = [].concat(this.rowIDs);
  53. };
  54. this.genData();
  55. return {
  56. dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  57. isLoading: false,
  58. };
  59. },
  60. onEndReached(event) {
  61. // load new data
  62. console.log('reach end', event);
  63. this.setState({ isLoading: true });
  64. setTimeout(() => {
  65. this.genData(++pageIndex);
  66. this.setState({
  67. dataSource: this.state.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  68. isLoading: false,
  69. });
  70. }, 1000);
  71. },
  72. render() {
  73. const separator = (sectionID, rowID) => (
  74. <div key={`${sectionID}-${rowID}`} style={{
  75. backgroundColor: '#F5F5F9',
  76. height: 8,
  77. borderTop: '1px solid #ECECED',
  78. borderBottom: '1px solid #ECECED',
  79. }} />
  80. );
  81. const row = (rowData, sectionID, rowID) => {
  82. if (index < 0) {
  83. index = data.length - 1;
  84. }
  85. const obj = data[index--];
  86. return (
  87. <div key={rowID}
  88. style={{
  89. padding: '8px 16px',
  90. backgroundColor: 'white',
  91. }}
  92. >
  93. <h3 style={{
  94. padding: 2,
  95. marginBottom: 8,
  96. borderBottom: '1px solid #F6F6F6',
  97. }}>{obj.title}</h3>
  98. <div style={{ display: '-webkit-box', display: 'flex' }}>
  99. <img style={{ height: 64 * (window.viewportScale || 1), marginRight: 8 }} src={obj.img} />
  100. <div style={{ display: 'inline-block' }}>
  101. <p>{obj.des}</p>
  102. <p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>35</span>元/任务</p>
  103. </div>
  104. </div>
  105. </div>
  106. );
  107. };
  108. return (<div style={{ margin: '0 auto', width: '96%' }}>
  109. <ListView
  110. dataSource={this.state.dataSource}
  111. renderHeader={() => <span>header</span>}
  112. renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
  113. {this.state.isLoading ? '加载中...' : '加载完毕'}
  114. </div>}
  115. renderSectionHeader={(sectionData) => (
  116. <div>{`任务 ${sectionData.split(' ')[1]}`}</div>
  117. )}
  118. renderRow={row}
  119. renderSeparator={separator}
  120. pageSize={4}
  121. scrollRenderAheadDistance={500}
  122. scrollEventThrottle={20}
  123. onScroll={() => { console.log('scroll'); }}
  124. onEndReached={this.onEndReached}
  125. onEndReachedThreshold={10}
  126. style={{
  127. height: document.body.clientHeight * 3 / 4,
  128. overflow: 'auto',
  129. border: '1px solid #ddd',
  130. margin: '10px 0',
  131. }}
  132. />
  133. </div>);
  134. },
  135. });
  136. ReactDOM.render(<Demo />, mountNode);

body 容器

使用 html 的 body 作为滚动容器

  1. /* eslint no-dupe-keys: 0 */
  2. import { ListView } from 'antd-mobile';
  3. const data = [
  4. {
  5. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  6. title: '相约酒店',
  7. des: '不是所有的兼职汪都需要风吹日晒',
  8. },
  9. {
  10. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  11. title: '麦当劳邀您过周末',
  12. des: '不是所有的兼职汪都需要风吹日晒',
  13. },
  14. {
  15. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  16. title: '食惠周',
  17. des: '不是所有的兼职汪都需要风吹日晒',
  18. },
  19. ];
  20. let index = data.length - 1;
  21. const NUM_ROWS = 20;
  22. let pageIndex = 0;
  23. const Demo = React.createClass({
  24. getInitialState() {
  25. const dataSource = new ListView.DataSource({
  26. rowHasChanged: (row1, row2) => row1 !== row2,
  27. });
  28. this.genData = (pIndex = 0) => {
  29. const dataBlob = {};
  30. for (let i = 0; i < NUM_ROWS; i++) {
  31. const ii = (pIndex * NUM_ROWS) + i;
  32. dataBlob[`${ii}`] = `row - ${ii}`;
  33. }
  34. return dataBlob;
  35. };
  36. this.rData = {};
  37. return {
  38. dataSource: dataSource.cloneWithRows(this.genData()),
  39. isLoading: false,
  40. };
  41. },
  42. onEndReached(event) {
  43. // load new data
  44. console.log('reach end', event);
  45. this.setState({ isLoading: true });
  46. setTimeout(() => {
  47. this.rData = { ...this.rData, ...this.genData(++pageIndex) };
  48. this.setState({
  49. dataSource: this.state.dataSource.cloneWithRows(this.rData),
  50. isLoading: false,
  51. });
  52. }, 1000);
  53. },
  54. render() {
  55. const separator = (sectionID, rowID) => (
  56. <div key={`${sectionID}-${rowID}`} style={{
  57. backgroundColor: '#F5F5F9',
  58. height: 8,
  59. borderTop: '1px solid #ECECED',
  60. borderBottom: '1px solid #ECECED',
  61. }} />
  62. );
  63. const row = (rowData, sectionID, rowID) => {
  64. if (index < 0) {
  65. index = data.length - 1;
  66. }
  67. const obj = data[index--];
  68. return (
  69. <div key={rowID}
  70. style={{
  71. padding: '8px 16px',
  72. backgroundColor: 'white',
  73. }}
  74. >
  75. <h3 style={{
  76. padding: 2,
  77. marginBottom: 8,
  78. borderBottom: '1px solid #F6F6F6',
  79. }}>{obj.title}</h3>
  80. <div style={{ display: '-webkit-box', display: 'flex' }}>
  81. <img style={{ height: 64 * (window.viewportScale || 1), marginRight: 8 }} src={obj.img} />
  82. <div style={{ display: 'inline-block' }}>
  83. <p>{obj.des}</p>
  84. <p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>35</span>元/任务</p>
  85. </div>
  86. </div>
  87. </div>
  88. );
  89. };
  90. return (<div>
  91. <ListView
  92. dataSource={this.state.dataSource}
  93. renderHeader={() => <span>header</span>}
  94. renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
  95. {this.state.isLoading ? '加载中...' : '加载完毕'}
  96. </div>}
  97. renderRow={row}
  98. renderSeparator={separator}
  99. pageSize={4}
  100. scrollRenderAheadDistance={500}
  101. scrollEventThrottle={20}
  102. onScroll={() => { console.log('scroll'); }}
  103. useBodyScroll
  104. onEndReached={this.onEndReached}
  105. onEndReachedThreshold={10}
  106. />
  107. </div>);
  108. },
  109. });
  110. ReactDOM.render(<Demo />, mountNode);

吸顶(body 容器)

区块标题 “吸顶”(sticky) 功能示例

  1. /* eslint no-dupe-keys: 0 */
  2. import { ListView } from 'antd-mobile';
  3. const data = [
  4. {
  5. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  6. title: '相约酒店',
  7. des: '不是所有的兼职汪都需要风吹日晒',
  8. },
  9. {
  10. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  11. title: '麦当劳邀您过周末',
  12. des: '不是所有的兼职汪都需要风吹日晒',
  13. },
  14. {
  15. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  16. title: '食惠周',
  17. des: '不是所有的兼职汪都需要风吹日晒',
  18. },
  19. ];
  20. let index = data.length - 1;
  21. const NUM_SECTIONS = 5;
  22. const NUM_ROWS_PER_SECTION = 5;
  23. let pageIndex = 0;
  24. const Demo = React.createClass({
  25. getInitialState() {
  26. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  27. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  28. const dataSource = new ListView.DataSource({
  29. getRowData,
  30. getSectionHeaderData: getSectionData,
  31. rowHasChanged: (row1, row2) => row1 !== row2,
  32. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  33. });
  34. this.dataBlob = {};
  35. this.sectionIDs = [];
  36. this.rowIDs = [];
  37. this.genData = (pIndex = 0) => {
  38. for (let i = 0; i < NUM_SECTIONS; i++) {
  39. const ii = (pIndex * NUM_SECTIONS) + i;
  40. const sectionName = `Section ${ii}`;
  41. this.sectionIDs.push(sectionName);
  42. this.dataBlob[sectionName] = sectionName;
  43. this.rowIDs[ii] = [];
  44. for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
  45. const rowName = `S${ii}, R${jj}`;
  46. this.rowIDs[ii].push(rowName);
  47. this.dataBlob[rowName] = rowName;
  48. }
  49. }
  50. // new object ref
  51. this.sectionIDs = [].concat(this.sectionIDs);
  52. this.rowIDs = [].concat(this.rowIDs);
  53. };
  54. this.genData();
  55. return {
  56. dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  57. isLoading: false,
  58. };
  59. },
  60. onEndReached(event) {
  61. // load new data
  62. console.log('reach end', event);
  63. this.setState({ isLoading: true });
  64. setTimeout(() => {
  65. this.genData(++pageIndex);
  66. this.setState({
  67. dataSource: this.state.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  68. isLoading: false,
  69. });
  70. }, 1000);
  71. },
  72. render() {
  73. const separator = (sectionID, rowID) => (
  74. <div key={`${sectionID}-${rowID}`} style={{
  75. backgroundColor: '#F5F5F9',
  76. height: 8,
  77. borderTop: '1px solid #ECECED',
  78. borderBottom: '1px solid #ECECED',
  79. }} />
  80. );
  81. const row = (rowData, sectionID, rowID) => {
  82. if (index < 0) {
  83. index = data.length - 1;
  84. }
  85. const obj = data[index--];
  86. return (
  87. <div key={rowID}
  88. style={{
  89. padding: '8px 16px',
  90. backgroundColor: 'white',
  91. }}
  92. >
  93. <h3 style={{
  94. padding: 2,
  95. marginBottom: 8,
  96. borderBottom: '1px solid #F6F6F6',
  97. }}>{obj.title}</h3>
  98. <div style={{ display: '-webkit-box', display: 'flex' }}>
  99. <img style={{ height: 64 * (window.viewportScale || 1), marginRight: 8 }} src={obj.img} />
  100. <div style={{ display: 'inline-block' }}>
  101. <p>{obj.des}</p>
  102. <p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>35</span>元/任务</p>
  103. </div>
  104. </div>
  105. </div>
  106. );
  107. };
  108. return (<div>
  109. <ListView
  110. dataSource={this.state.dataSource}
  111. renderHeader={() => <span>header</span>}
  112. renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
  113. {this.state.isLoading ? '加载中...' : '加载完毕'}
  114. </div>}
  115. renderSectionHeader={(sectionData) => (
  116. <div>{`任务 ${sectionData.split(' ')[1]}`}</div>
  117. )}
  118. renderRow={row}
  119. renderSeparator={separator}
  120. pageSize={4}
  121. scrollEventThrottle={20}
  122. onScroll={() => { console.log('scroll'); }}
  123. onEndReached={this.onEndReached}
  124. onEndReachedThreshold={10}
  125. stickyHeader
  126. stickyProps={{
  127. stickyStyle: { zIndex: 999 },
  128. // topOffset: -43,
  129. // isActive: false, // 关闭 sticky 效果
  130. }}
  131. />
  132. </div>);
  133. },
  134. });
  135. ReactDOM.render(<Demo />, mountNode);

IndexedList

用于通讯薄等场景

  1. /* eslint no-mixed-operators: 0 */
  2. import province from 'site/data/province';
  3. import { ListView, List } from 'antd-mobile';
  4. const { Item } = List;
  5. const Demo = React.createClass({
  6. getInitialState() {
  7. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  8. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  9. const dataSource = new ListView.DataSource({
  10. getRowData,
  11. getSectionHeaderData: getSectionData,
  12. rowHasChanged: (row1, row2) => row1 !== row2,
  13. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  14. });
  15. const dataBlob = {};
  16. const sectionIDs = [];
  17. const rowIDs = [];
  18. Object.keys(province).forEach((item, index) => {
  19. sectionIDs.push(item);
  20. dataBlob[item] = item;
  21. rowIDs[index] = [];
  22. province[item].forEach(jj => {
  23. rowIDs[index].push(jj.value);
  24. dataBlob[jj.value] = jj.label;
  25. });
  26. });
  27. return {
  28. dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
  29. headerPressCount: 0,
  30. };
  31. },
  32. render() {
  33. return (<div>
  34. <div style={{ position: 'relative' }}>
  35. <ListView.IndexedList
  36. dataSource={this.state.dataSource}
  37. renderHeader={() => <span>头部内容请自定义</span>}
  38. renderFooter={() => <span>尾部内容请自定义</span>}
  39. renderSectionHeader={(sectionData) => (<div>{sectionData}</div>)}
  40. renderRow={(rowData) => (<Item>{rowData}</Item>)}
  41. style={{
  42. height: document.body.clientHeight * 3 / 4,
  43. overflow: 'auto',
  44. }}
  45. quickSearchBarStyle={{
  46. position: 'absolute',
  47. top: 20,
  48. }}
  49. delayTime={10}
  50. delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
  51. />
  52. </div>
  53. </div>);
  54. },
  55. });
  56. ReactDOM.render(<Demo />, mountNode);

IndexedList 吸顶

用于通讯薄等场景 “吸顶”(sticky)

  1. import provinceData from 'site/data/province';
  2. import { ListView, List, SearchBar } from 'antd-mobile';
  3. const { Item } = List;
  4. const Demo = React.createClass({
  5. getInitialState() {
  6. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  7. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  8. const dataSource = new ListView.DataSource({
  9. getRowData,
  10. getSectionHeaderData: getSectionData,
  11. rowHasChanged: (row1, row2) => row1 !== row2,
  12. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  13. });
  14. this.createDs = (ds, province) => {
  15. const dataBlob = {};
  16. const sectionIDs = [];
  17. const rowIDs = [];
  18. Object.keys(province).forEach((item, index) => {
  19. sectionIDs.push(item);
  20. dataBlob[item] = item;
  21. rowIDs[index] = [];
  22. province[item].forEach(jj => {
  23. rowIDs[index].push(jj.value);
  24. dataBlob[jj.value] = jj.label;
  25. });
  26. });
  27. return ds.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs);
  28. };
  29. return {
  30. inputValue: '',
  31. dataSource: this.createDs(dataSource, provinceData),
  32. headerPressCount: 0,
  33. };
  34. },
  35. onSearch(val) {
  36. const pd = { ...provinceData };
  37. Object.keys(pd).forEach((item) => {
  38. pd[item] = pd[item].filter(jj => jj.spell.toLocaleLowerCase().indexOf(val) > -1);
  39. });
  40. this.setState({
  41. inputValue: val,
  42. dataSource: this.createDs(this.state.dataSource, pd),
  43. });
  44. },
  45. render() {
  46. return (<div style={{ paddingTop: 44 * (window.viewportScale || 1) }}>
  47. <div style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
  48. <SearchBar
  49. value={this.state.inputValue}
  50. placeholder="搜索"
  51. onChange={this.onSearch}
  52. onClear={() => { console.log('onClear'); }}
  53. onCancel={() => { console.log('onCancel'); }}
  54. />
  55. </div>
  56. <ListView.IndexedList
  57. dataSource={this.state.dataSource}
  58. renderHeader={() => <span>头部内容请自定义</span>}
  59. renderFooter={() => <span>尾部内容请自定义</span>}
  60. renderSectionHeader={(sectionData) => (<div>{sectionData}</div>)}
  61. renderRow={(rowData) => (<Item>{rowData}</Item>)}
  62. stickyHeader
  63. stickyProps={{
  64. stickyStyle: { zIndex: 999 },
  65. }}
  66. quickSearchBarStyle={{
  67. top: 85,
  68. }}
  69. delayTime={10}
  70. delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
  71. />
  72. </div>);
  73. },
  74. });
  75. ReactDOM.render(<Demo />, mountNode);

ListView长列表 - 图1

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: 注意:只支持onLayout prop

新增API (web)

  • useBodyScroll (boolean, false) - 使用 html 的 body 作为滚动容器

  • stickyHeader 固定区块标题到页面顶部 (注意: 设置后,ScrollComponent 将被渲染到 body 的第一个元素里,使用 html 的 body 作为滚动容器)

  • 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 指示器