import { IStoreState } from '../../index.data'; import { getAllCollapsedRowFlags } from '../../store/selectors'; import * as R from 'rambda'; import { getFrozenHeight } from '../../store/selectors/dimensions'; // 此处计算时, 涉及一些重复计算 // 如果使用memoize, 由于参数是state, 被memo后老的state无法释放, 容易导致内存耗尽 // 所以只好改用class export class VScrollBarHelper { public barOffset: number; public barSize: number; // public thumbSize: number; // public thumbOffset: number; private _allVisibleRowHeight: number = 0; private state: IStoreState; constructor(state: IStoreState) { this.state = state; if ( state.containerBox.w <= 0 || state.containerBox.h <= 0 || R.isEmpty(state.colDimensions) || R.isEmpty(state.rowDimensions) ) { this.barOffset = 0; this.barSize = 0; // this.thumbOffset = 0; // this.thumbSize = 0; } else { this.barOffset = this.getBarOffset(); this.barSize = this.getBarSize(); this._allVisibleRowHeight = get_allVisibleRowHeight_exceptFrozen(this.state); // this.thumbSize = this.getThumbSize(); // this.thumbOffset = this.getThumbOffset(); } } // 滚动条从哪里开始 ( sum(FreezedRowHeight), 不用换算 ) getBarOffset() { return getFrozenHeight(this.state) + 20 /*col indicator*/; } // 滚动条有多高 ( containerHeight - barOffset - 底部留白20防止与HScrollBar相交 ) getBarSize() { const state = this.state; return Math.max(state.containerBox.h - this.barOffset - 20, 0) /* 留白, 防止与HScrollBar相交 */; } // 滚动指示器从哪里开始 // 假设滚动条滚到最下面: // 算法1: // 1. 内容区可滚动最大范围(max-scrollY) a: (allVisibleRowHeight + 500预留) - containerHeight, 此时不可再下滚 // 2. 滚动条最多拖动距离 b : barHeight - thumbSize // 3. b/a: 内容区滚动1个单位, 滚动条对应移动多少 // 等价算法2: barSize / containerHeight // 返回 b/a * scrollY getThumbOffset() { const state = this.state; if (state.containerBox.h - this.barOffset <= 0) return 0; return (this.barSize / (this._allVisibleRowHeight + state.padding.bottom)) * state.scrollY; } // 滚动指示器有多高 ( barHeight / (allVisibleRowHeight_except_freezed + 500) * barHeight ) getThumbSize() { const state = this.state; return ( (this.barSize / (this._allVisibleRowHeight + state.padding.bottom)) * (this.barSize + 20) /*即容器真实高度 - 冻结行高度*/ ); } } // region 私有方法begin // 行是否被冻结 function isFrozenRowByI(state: IStoreState, i: number): boolean { return i <= state.freezeAt.i; } // function isHiddenRowByI_fast(state: IStoreState, collapsedIds: ID[], i: number): boolean { // return !!collapsedIds.find(id => id === state.rows[i].id); // } export function get_allVisibleRowHeight_exceptFrozen(state: IStoreState): number { const { rows, rowDimensions } = state; // for performance // const collapsedIds = getAllCollapsedRowIds(state); const collapsedFlags = getAllCollapsedRowFlags(state); // for performance let height = 0; for (let i = 0; i < rows.length; i++) { const row = rows[i]; // if (!isFrozenRowByI(state, i) && !isHiddenRowByI_fast(state, collapsedIds, i)) { if (!isFrozenRowByI(state, i) && !collapsedFlags[i]) { const rowDim = rowDimensions[row.id]; height += rowDim ? rowDim.height : 30; } } return height; } // endregion /** * 找到某个区域内的全部可加载行, 包括可见行和预加载行 及 冻结行 * @param state * @param clipHeight 区域高度 * @return [true, false, false...], true表示可见, 数组长度等于state.rows.length */ export function getAllLoadableRowFlags(state: IStoreState): boolean[] { const loadingHeight = state.containerBox.h + state.padding.bottom; // const collapsedIds = getAllCollapsedRowIds(state); const collapsedFlags = getAllCollapsedRowFlags(state); // return _getAllLoadableRowFlags(state, loadingHeight, collapsedIds, state.scrollY); return _getAllLoadableRowFlags(state, loadingHeight, collapsedFlags, state.scrollY); } // 找到某个区域内的全部可加载行, 包括可见行和预加载行 及 冻结行 ( 内部使用 ) // @param state // @param loadingHeight 可加载区域的高度, = 视口高度 + 预加载区域高度 // @param collapsedFlags 被折叠/隐藏的row的flag数组 // @param scrollY ( 注意这是针对非冻结区域的 ) // @return rows export function _getAllLoadableRowFlags( state: IStoreState, loadingHeight: number, collapsedFlags: boolean[], scrollY: number, ): boolean[] { if (state.freezeAt.i < 0) { return R.repeat(true, state.rows.length); } // console.log('计算时, 一定要注意: scrollY 不包含在frozen区域中'); const { rows, rowDimensions } = state; const freezeAtI = state.freezeAt.i; const result = R.repeat(false, rows.length); for (let i = 0; i < freezeAtI + 1; i++) { result[i] = true; } const lastFrozenRow = freezeAtI >= 0 ? rows[freezeAtI] : null; // ------------------------ 冻结行 1, 进入usedHeight // ------------------------ 冻结行 2, 进入usedHeight // ------------------------ 正常行 3, 进入usedHeight // ------------------------ 正常行 4, 进入usedHeight // ------------------------ 隐藏行 5 // ------------------------ 正常行 6, 进入usedHeight // ------------------------ 折叠行 7 const frozenHeight = lastFrozenRow ? rowDimensions[lastFrozenRow.id].height + rowDimensions[lastFrozenRow.id].top : 0; let usedHeight = frozenHeight; for (let i = freezeAtI + 1; i < rows.length; i++) { // 3. 排除视口以下的 if (usedHeight >= scrollY + loadingHeight) break; // 由于新插入的行, 在rowDimensions中没有对应值, 那么应该设为30, 还是设为0呢? // 设为0, 则自动放到loadable数组, 下次循环进行resetDimensions计算出正确dimensions, 再下个循环设置 选框位置 // 设为30, 并强制将新行加入loadable数组, 也是可以的, 但意义不大 const rowDim = rowDimensions[rows[i].id]; if (!rowDim) { result[i] = true; continue; } const rowHeight = rowDim.height; // 2. 排除视口以上的 if (usedHeight + rowHeight <= scrollY + frozenHeight) { usedHeight += rowHeight; continue; } // 3. 排除被collapsed的 // if (isHiddenRowByI_fast(state, collapsedIds, i)) continue; if (collapsedFlags[i]) continue; result[i] = true; usedHeight += rowHeight; } return result; }