如何使用具有反应虚拟化列表的可扩展行来排除行高调整?

时间:2018-06-05 14:56:33

标签: reactjs material-ui react-virtualized

我在具有反应虚拟化列表的行中使用可扩展面板(Material-UI)并且一直存在高度自动调整问题。我已经在反应虚拟化网站上阅读了几篇SO帖子以及关于动态行高的一些问题,但是我有一个特定的问题,似乎有一个'关闭一个'在展开/折叠面板后调整行高时出现问题。

这是预期的行为:

  1. 默认情况下展开了行面板。
  2. 用户点击可展开的面板行。
  3. 行面板折叠。
  4. 行高调整为面板折叠。
  5. 以下是第一次点击的实际行为:

    1. 默认情况下展开了行面板。
    2. 用户点击可展开的面板行。
    3. 行面板折叠。
    4. 行高不会调整为面板折叠。
    5. 但是,在随后的点击中,行高度会进行调整,但是对应于'状态,导致不一致 - 即当单击行面板再次展开时,行高度调整为行高度,就好像它是折叠的,反之亦然。因此,当面板折叠后,在它之后有一堆空白区域,当它在技术上扩展时,行高度太小而无法看到内容。
    6. 除了发布代码并注意onRowClick()在面板折叠/展开时触发时,我不确定要包含哪些其他信息。

      这里是父组件:

      import React, { Component } from 'react';
      import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
      import List from 'react-virtualized/dist/commonjs/List';
      import { CellMeasurer, CellMeasurerCache } from 'react-virtualized/dist/commonjs/CellMeasurer';
      import EquipSummaryRow from './EquipSummaryRow';
      import './EquipSummary.css';
      
      class EquipSummary extends Component {
        constructor(props) {
          super(props);
      
          this.cache = new CellMeasurerCache({
            fixedWidth: true,
          });
      
          this.rowRenderer = this.rowRenderer.bind(this);
          this.getDatum = this.getDatum.bind(this);
          this.onRowClick = this.onRowClick.bind(this);
        }
      
        getDatum(index) {
          const list = this.props.equipData;
      
          return list[index];
        }
      
        saveRef = (ref) => this.containerNode = ref;
      
        saveListRef = (ref) => {
          this.list = ref;
        }
      
        componentDidUpdate() {
          console.log('component updated');
          this.cache.clearAll();
          this.list.recomputeRowHeights();
        }
      
        onRowClick(e, index) {
          e.preventDefault();
          this.cache.clear(index);
          this.list.recomputeRowHeights();
          this.list.forceUpdateGrid();
        }
      
        rowRenderer({ index, key, parent, style }) {
          const datum = this.getDatum(index);
          return (
            <div key={key} style={style}>
              <CellMeasurer
                cache={this.cache}
                columnIndex={0}
                key={key}
                rowIndex={index}
                parent={parent}
              >
                {({ measure }) => (
                  <EquipSummaryRow
                    onClick={(e, idx) => this.onRowClick(e, idx)}
                    measure={measure}
                    datum={datum}
                    index={index}
                  />
                )}
              </CellMeasurer>
            </div>
          );
        }
      
        render() {
          console.log('rendering..');
          return (
            <div className="EquipSummary-AutoSizer" ref={this.saveRef}>
              <AutoSizer>
                {({ width, height }) => (
                  <List
                    ref={this.saveListRef}
                    width={width}
                    height={height}
                    rowHeight={this.cache.rowHeight}
                    rowCount={this.props.equipData.length}
                    rowRenderer={this.rowRenderer}
                    deferredMeasurementCache={this.cache}
                    equipData={this.props.equipData}
                  />
                )}
              </AutoSizer>
            </div>
          );
        }
      }
      
      export default EquipSummary;
      

      这是代表一行的组件:

      import React, { Component } from 'react';
      import {
        Table,
        TableBody,
        TableHeader,
        TableHeaderColumn,
        TableRow,
        TableRowColumn,
      } from 'material-ui/Table';
      import { MuiThemeProvider } from 'material-ui/styles';
      import ExpansionPanel from '@material-ui/core/ExpansionPanel';
      import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
      import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
      import Typography from '@material-ui/core/Typography';
      
      
      class EquipSummaryRow extends Component {
        render() {
          const { datum } = this.props;
      
          return (
            <div>
              <ExpansionPanel
                defaultExpanded
                onChange={e => this.props.onClick(e, this.props.index)}
              >
                <ExpansionPanelSummary expandIcon={<div>|</div>}>
                  <Typography>{`${datum.type}      (id: ${datum.instance}, points: ${datum.points.length})`}</Typography>
                </ExpansionPanelSummary>
                <ExpansionPanelDetails>
                  <Table>
                    <TableHeader
                      displaySelectAll={false}
                      adjustForCheckbox={false}
                    >
                      <TableRow>
                        <TableHeaderColumn>Device</TableHeaderColumn>
                        <TableHeaderColumn>Object ID</TableHeaderColumn>
                        <TableHeaderColumn>Type</TableHeaderColumn>
                        <TableHeaderColumn>Name</TableHeaderColumn>
                        <TableHeaderColumn>Description</TableHeaderColumn>
                        <TableHeaderColumn>Units</TableHeaderColumn>
                        <TableHeaderColumn>Value</TableHeaderColumn>
                      </TableRow>
                    </TableHeader>
                    <TableBody
                      displayRowCheckbox={false}
                    >
                      {datum.points.map((row, index) => (
                        <TableRow key={row.id}>
                          <TableRowColumn>{row.device}</TableRowColumn>
                          <TableRowColumn>{row.objectID}</TableRowColumn>
                          <TableRowColumn>{row.type}</TableRowColumn>
                          <TableRowColumn>{row.name}</TableRowColumn>
                          <TableRowColumn>{row.description}</TableRowColumn>
                          <TableRowColumn>{row.units}</TableRowColumn>
                          <TableRowColumn>{row.value}</TableRowColumn>
                        </TableRow>
                        ))}
                    </TableBody>
                  </Table>
                </ExpansionPanelDetails>
              </ExpansionPanel>
            </div>
          );
        }
      }
      
      export default EquipSummaryRow;
      

      这可能是我如何使用缓存的问题吗?我一直在打我的头,所以任何建议都值得赞赏!

2 个答案:

答案 0 :(得分:1)

弄清楚我的问题。问题是Material-UI可扩展面板具有动画崩溃,因此在面板到达其展开/折叠形式之间存在延迟。 &#39; onChange&#39;事件立即触发,以便在动画发生时进行测量。我目前正试图找出一种在动画结束后触发测量的方法,但这不是反应虚拟化的问题。

答案 1 :(得分:0)

(这不是一个完整的答案,但是它确实允许动画步骤按设计进行。如果有足够的时间,我认为这可以完全进行。请参阅我的评论以获取更多信息。)

List组件中,有一个选项可以传递不同的cellRangeRenderer。该cellRangeRenderer是负责生成附加到每个单独单元格的style对象的原因。默认的cellRangeRenderer使用绝对定位来完成此操作。我创建了一个修改后的cellRangeRenderer,实际上并未在style对象中设置任何有效的内容,而是为单元格生成了一个容器。容器使用绝对定位来显示单元格相对于滚动条的位置,但是在容器内部,每个单元格均按原样呈现。

import React from 'react'

/**
 * Default implementation of cellRangeRenderer used by Grid.
 * This renderer supports cell-caching while the user is scrolling.
 */

export default function cellRangeRenderer({
  cellCache,
  cellRenderer,
  columnSizeAndPositionManager,
  columnStartIndex,
  columnStopIndex,
  deferredMeasurementCache,
  horizontalOffsetAdjustment,
  isScrolling,
  isScrollingOptOut,
  parent, // Grid (or List or Table)
  rowSizeAndPositionManager,
  rowStartIndex,
  rowStopIndex,
  styleCache,
  verticalOffsetAdjustment,
  visibleColumnIndices,
  visibleRowIndices,
}) {
  const renderedCells = [];

  // Browsers have native size limits for elements (eg Chrome 33M pixels, IE 1.5M pixes).
  // User cannot scroll beyond these size limitations.
  // In order to work around this, ScalingCellSizeAndPositionManager compresses offsets.
  // We should never cache styles for compressed offsets though as this can lead to bugs.
  // See issue #576 for more.
  const areOffsetsAdjusted = columnSizeAndPositionManager.areOffsetsAdjusted() || rowSizeAndPositionManager.areOffsetsAdjusted();

  const canCacheStyle = !isScrolling && !areOffsetsAdjusted;
  let styledBuffer = false
  let bufferStyle, containerStyle

  for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
    const rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex);

    for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
      const columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex);
      const isVisible = columnIndex >= visibleColumnIndices.start && columnIndex <= visibleColumnIndices.stop && rowIndex >= visibleRowIndices.start && rowIndex <= visibleRowIndices.stop;
      const key = `${rowIndex}-${columnIndex}`;
      let style;
      // this is the part that bugs out when react-virtualized re-renders part of the what's-showing-now list, rather than the entire what's-showing-now list
      // I'm just grabbing the first cell and assuming it's coordinates are the top of the what's-showing-now list
      if (!styledBuffer) {
        styledBuffer = true
        bufferStyle = {
          position: 'absolute',
          top: 0,
          left: 0,
          height: rowDatum.offset + verticalOffsetAdjustment,
          width: columnDatum.offset + horizontalOffsetAdjustment,
        }
        containerStyle = {
          position: 'absolute',
          top: rowDatum.offset + verticalOffsetAdjustment,
          left: columnDatum.offset + horizontalOffsetAdjustment,
          height: 'auto',
          width: 'auto',
        }
      }

      // Cache style objects so shallow-compare doesn't re-render unnecessarily.
      if (canCacheStyle && styleCache[key]) {
        style = styleCache[key];
      } else if (deferredMeasurementCache && !deferredMeasurementCache.has(rowIndex, columnIndex)) {
      // In deferred mode, cells will be initially rendered before we know their size.
      // Don't interfere with CellMeasurer's measurements by setting an invalid size.
        // Position not-yet-measured cells at top/left 0,0,
        // And give them width/height of 'auto' so they can grow larger than the parent Grid if necessary.
        // Positioning them further to the right/bottom influences their measured size.
        style = {
          height: 'auto',
          left: 0,
          position: 'absolute',
          top: 0,
          width: 'auto'
        };
      } else {
        // I'd go with a completely empty object, but that breaks other parts of react-virtualized that rely, at least, on 'width' being defined
        style = {
          height: 'auto',
          width: 'auto',
        }
        styleCache[key] = style;
      }

      const cellRendererParams = {
        columnIndex,
        isScrolling,
        isVisible,
        key,
        parent,
        rowIndex,
        style
      };

      let renderedCell;

      // Avoid re-creating cells while scrolling.
      // This can lead to the same cell being created many times and can cause performance issues for "heavy" cells.
      // If a scroll is in progress- cache and reuse cells.
      // This cache will be thrown away once scrolling completes.
      // However if we are scaling scroll positions and sizes, we should also avoid caching.
      // This is because the offset changes slightly as scroll position changes and caching leads to stale values.
      // For more info refer to issue #395
      //
      // If isScrollingOptOut is specified, we always cache cells.
      // For more info refer to issue #1028
      if ((isScrollingOptOut || isScrolling) && !horizontalOffsetAdjustment && !verticalOffsetAdjustment) {
        if (!cellCache[key]) {
          cellCache[key] = cellRenderer(cellRendererParams);
        }

        renderedCell = cellCache[key];

        // If the user is no longer scrolling, don't cache cells.
        // This makes dynamic cell content difficult for users and would also lead to a heavier memory footprint.
      } else {
        renderedCell = cellRenderer(cellRendererParams);
      }

      if (renderedCell === null || renderedCell === false) {
        continue;
      }

      if (process.env.NODE_ENV !== 'production') {
        warnAboutMissingStyle(parent, renderedCell);
      }

      renderedCells.push(renderedCell);
    }
  }

  // This is where the new "magic" happens
  return [(
    <div id="0-buffer-at-the-top" key="0-buffer-at-the-top" style={bufferStyle} />
  ), (
    <div id="0-container-at-the-top" key="0-container-at-the-top" style={containerStyle}>
      {renderedCells}
    </div>
  )];
}

function warnAboutMissingStyle(parent, renderedCellParam) {
  let renderedCell = renderedCellParam
  if (process.env.NODE_ENV !== 'production') {
    if (renderedCell) {
      // If the direct child is a CellMeasurer, then we should check its child
      // See issue #611
      if (renderedCell.type && renderedCell.type.__internalCellMeasurerFlag) {
        renderedCell = renderedCell.props.children;
      }

      if (renderedCell && renderedCell.props && renderedCell.props.style === undefined && parent.__warnedAboutMissingStyle !== true) {
        parent.__warnedAboutMissingStyle = true;

        console.warn('Rendered cell should include style property for positioning.');
      }
    }
  }
}

此代码从npm软件包中分发的内容的副本开始(从某种程度上绕过babel编译步骤)开始。至少有以下问题:

  • 必须在列表(而不是网格)中使用它。网格要求将单元正确放置在网格(材料UI网格,而不是反应虚拟化网格)中,而不是直接扔在那里。
  • reli-virtualized有一些优化,允许在列表的子部分调用此方法,而不是渲染整个块(解决此问题的时间超出了我尝试进行此修复的时间范围)。如果解决了此问题,那么新的cellRangeRenderer可以按原样正确运行90%。
  • 因为您可以展开一行然后滚动,所以行大小仍然需要CellMeasurer来计算高度。因为我没有将高度应用于每个单独的单元格,所以我们需要以一种稍微聪明的方式使用高度重新计算容器的“顶部”高度。仅当您完全滚动到显示扩展面板的部分时,此选项才会损坏。仅将高度应用于style对象可能就足够了,但这未经测试。编辑:您仍然需要按照暗示的方式将呼叫延迟到measure
  • 跳到特定单元格尚未经过测试,可能会或可能不会。