ListView 长列表

定义/Definition

react-native 核心组件。 高性能列表:节点分步渲染;无尽列表。

规则 / Rule

常用在渲染长列表中

代码演示

基本

无尽列表

  1. import { ListView, Toast, Button } from 'antd-mobile';
  2. const data = [
  3. {
  4. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  5. title: '相约酒店',
  6. des: '不是所有的兼职汪都需要风吹日晒',
  7. },
  8. {
  9. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  10. title: '麦当劳邀您过周末',
  11. des: '不是所有的兼职汪都需要风吹日晒',
  12. },
  13. {
  14. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  15. title: '食惠周',
  16. des: '不是所有的兼职汪都需要风吹日晒',
  17. },
  18. ];
  19. let index = data.length - 1;
  20. const NUM_SECTIONS = 5;
  21. const NUM_ROWS_PER_SECTION = 5;
  22. let pageIndex = 0;
  23. const Demo = React.createClass({
  24. getInitialState() {
  25. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  26. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  27. const dataSource = new ListView.DataSource({
  28. getRowData,
  29. getSectionHeaderData: getSectionData,
  30. rowHasChanged: (row1, row2) => row1 !== row2,
  31. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  32. });
  33. this.dataBlob = {};
  34. this.sectionIDs = [];
  35. this.rowIDs = [];
  36. this.genData = (pIndex = 0) => {
  37. for (let i = 0; i < NUM_SECTIONS; i++) {
  38. const ii = pIndex * NUM_SECTIONS + i;
  39. const sectionName = `Section ${ii}`;
  40. this.sectionIDs.push(sectionName);
  41. this.dataBlob[sectionName] = sectionName;
  42. this.rowIDs[ii] = [];
  43. for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
  44. const rowName = `S${ii}, R${jj}`;
  45. this.rowIDs[ii].push(rowName);
  46. this.dataBlob[rowName] = rowName;
  47. }
  48. }
  49. // new object ref
  50. this.sectionIDs = [].concat(this.sectionIDs);
  51. this.rowIDs = [].concat(this.rowIDs);
  52. };
  53. this.genData();
  54. return {
  55. dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  56. isLoading: false,
  57. };
  58. },
  59. onEndReached(event) {
  60. // load new data
  61. console.log('reach end', event);
  62. Toast.info('加载新数据');
  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. }}></div>
  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: 'flex' }}>
  99. <img style={{ height: 64, marginRight: 8 }} src={obj.img} />
  100. <div>
  101. <p>{obj.des}</p>
  102. <p><span style={{ fontSize: 24, color: '#FF6E27' }}>35</span>元/任务</p>
  103. </div>
  104. </div>
  105. </div>
  106. );
  107. };
  108. this.ctrlBodyScroll(false, true);
  109. return (<div style={{ margin: '0 auto', width: '96%' }}>
  110. <ListView
  111. dataSource={this.state.dataSource}
  112. renderHeader={() => <span>header</span>}
  113. renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
  114. {this.state.isLoading ? '加载中...' : '加载完毕'}
  115. </div>}
  116. renderSectionHeader={(sectionData) => (
  117. <div>{`任务 ${sectionData.split(' ')[1]}`}</div>
  118. )}
  119. renderRow={row}
  120. renderSeparator={separator}
  121. pageSize={4}
  122. scrollRenderAheadDistance={500}
  123. scrollEventThrottle={20}
  124. onScroll={() => { console.log('scroll'); }}
  125. onEndReached={this.onEndReached}
  126. onEndReachedThreshold={10}
  127. style={{ height: 300, overflow: 'auto', border: '1px solid #ddd', margin: '10px 0' }}
  128. />
  129. <div>
  130. <p>切换`body``overflow`样式:</p>
  131. <Button inline size="small" onClick={() => { this.ctrlBodyScroll(true); }}>auto</Button>&nbsp;
  132. <Button inline size="small" onClick={() => { this.ctrlBodyScroll(false); }}>hidden</Button>
  133. </div>
  134. </div>);
  135. },
  136. ctrlBodyScroll(flag, init) {
  137. document.body.style.overflowY = flag ? 'auto' : 'hidden';
  138. if (parent && parent !== self && !init) {
  139. parent.document.body.style.overflowY = flag ? 'auto' : 'hidden';
  140. }
  141. },
  142. });
  143. ReactDOM.render(<Demo />, mountNode);

sticky

无尽列表 sticky

  1. import { ListView, Toast } from 'antd-mobile';
  2. const data = [
  3. {
  4. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  5. title: '相约酒店',
  6. des: '不是所有的兼职汪都需要风吹日晒',
  7. },
  8. {
  9. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  10. title: '麦当劳邀您过周末',
  11. des: '不是所有的兼职汪都需要风吹日晒',
  12. },
  13. {
  14. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  15. title: '食惠周',
  16. des: '不是所有的兼职汪都需要风吹日晒',
  17. },
  18. ];
  19. let index = data.length - 1;
  20. const NUM_SECTIONS = 5;
  21. const NUM_ROWS_PER_SECTION = 5;
  22. let pageIndex = 0;
  23. const Demo = React.createClass({
  24. getInitialState() {
  25. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  26. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  27. const dataSource = new ListView.DataSource({
  28. getRowData,
  29. getSectionHeaderData: getSectionData,
  30. rowHasChanged: (row1, row2) => row1 !== row2,
  31. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  32. });
  33. this.dataBlob = {};
  34. this.sectionIDs = [];
  35. this.rowIDs = [];
  36. this.genData = (pIndex = 0) => {
  37. for (let i = 0; i < NUM_SECTIONS; i++) {
  38. const ii = pIndex * NUM_SECTIONS + i;
  39. const sectionName = `Section ${ii}`;
  40. this.sectionIDs.push(sectionName);
  41. this.dataBlob[sectionName] = sectionName;
  42. this.rowIDs[ii] = [];
  43. for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
  44. const rowName = `S${ii}, R${jj}`;
  45. this.rowIDs[ii].push(rowName);
  46. this.dataBlob[rowName] = rowName;
  47. }
  48. }
  49. // new object ref
  50. this.sectionIDs = [].concat(this.sectionIDs);
  51. this.rowIDs = [].concat(this.rowIDs);
  52. };
  53. this.genData();
  54. return {
  55. dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  56. isLoading: false,
  57. };
  58. },
  59. onEndReached(event) {
  60. // load new data
  61. console.log('reach end', event);
  62. Toast.info('加载新数据');
  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. }}></div>
  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: 'flex' }}>
  99. <img style={{ height: 64, marginRight: 8 }} src={obj.img} />
  100. <div>
  101. <p>{obj.des}</p>
  102. <p><span style={{ fontSize: 24, 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, top: 43 },
  128. topOffset: -43,
  129. // isActive: false, // 关闭 sticky 效果
  130. }}
  131. />
  132. </div>);
  133. },
  134. });
  135. ReactDOM.render(<Demo />, mountNode);

IndexedList

用于通讯薄等场景

  1. import province 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. const dataBlob = {};
  15. const sectionIDs = [];
  16. const rowIDs = [];
  17. Object.keys(province).forEach((item, index) => {
  18. sectionIDs.push(item);
  19. dataBlob[item] = item;
  20. rowIDs[index] = [];
  21. province[item].forEach(jj => {
  22. rowIDs[index].push(jj.value);
  23. dataBlob[jj.value] = jj.label;
  24. });
  25. });
  26. return {
  27. dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
  28. headerPressCount: 0,
  29. };
  30. },
  31. render() {
  32. return (<div style={{ paddingTop: 40 }}>
  33. <div style={{ position: 'fixed', zIndex: 999, top: 43, left: 0, right: 0 }}>
  34. <SearchBar
  35. value=""
  36. placeholder="搜索"
  37. onSubmit={(value) => { console.log(`onSubmit${value}`); }}
  38. onChange={(value) => { console.log(value); }}
  39. onClear={() => { console.log('onClear'); }}
  40. onCancel={() => { console.log('onCancel'); }}
  41. onFocus={() => { console.log('onFocus'); }}
  42. onBlur={() => { console.log('onBlur'); }}
  43. />
  44. </div>
  45. <div style={{ position: 'relative' }}>
  46. <ListView.IndexedList
  47. dataSource={this.state.dataSource}
  48. renderHeader={() => <span>头部内容请自定义</span>}
  49. renderFooter={() => <span>尾部内容请自定义</span>}
  50. renderSectionHeader={(sectionData) => (<div>{sectionData}</div>)}
  51. renderRow={(rowData) => (<Item>{rowData}</Item>)}
  52. style={{ height: 400, overflow: 'auto' }}
  53. quickSearchBarStyle={{
  54. position: 'absolute',
  55. top: 20, right: 10,
  56. }}
  57. delayTime={10}
  58. delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
  59. />
  60. </div>
  61. </div>);
  62. },
  63. });
  64. ReactDOM.render(<Demo />, mountNode);

IndexedList sticky

用于通讯薄等场景 sticky

  1. import province 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. const dataBlob = {};
  15. const sectionIDs = [];
  16. const rowIDs = [];
  17. Object.keys(province).forEach((item, index) => {
  18. sectionIDs.push(item);
  19. dataBlob[item] = item;
  20. rowIDs[index] = [];
  21. province[item].forEach(jj => {
  22. rowIDs[index].push(jj.value);
  23. dataBlob[jj.value] = jj.label;
  24. });
  25. });
  26. return {
  27. dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
  28. headerPressCount: 0,
  29. };
  30. },
  31. render() {
  32. return (<div style={{ paddingTop: 40 }}>
  33. <div style={{ position: 'fixed', zIndex: 999, top: 43, left: 0, right: 0 }}>
  34. <SearchBar
  35. value=""
  36. placeholder="搜索"
  37. onSubmit={(value) => { console.log(`onSubmit${value}`); }}
  38. onChange={(value) => { console.log(value); }}
  39. onClear={() => { console.log('onClear'); }}
  40. onCancel={() => { console.log('onCancel'); }}
  41. onFocus={() => { console.log('onFocus'); }}
  42. onBlur={() => { console.log('onBlur'); }}
  43. />
  44. </div>
  45. <ListView.IndexedList
  46. dataSource={this.state.dataSource}
  47. renderHeader={() => <span>头部内容请自定义</span>}
  48. renderFooter={() => <span>尾部内容请自定义</span>}
  49. renderSectionHeader={(sectionData) => (<div>{sectionData}</div>)}
  50. renderRow={(rowData) => (<Item>{rowData}</Item>)}
  51. stickyHeader
  52. stickyProps={{
  53. stickyStyle: { zIndex: 999, top: 83 },
  54. topOffset: -83,
  55. }}
  56. quickSearchBarStyle={{
  57. top: 85,
  58. }}
  59. delayTime={10}
  60. delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
  61. />
  62. </div>);
  63. },
  64. });
  65. 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

  • stickyHeader 固定区块标题到页面顶部 (注意: 设置后,ScrollComponent 将被渲染到 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 指示器