Skip to content

Commit d3ca82f

Browse files
committed
perf(viewer-ohos): 实施 DisplaySync 硬件 VSync 滚动与合并单元格分桶索引
Signed-off-by: donglua <dongluh@gmail.com>
1 parent 79d10f1 commit d3ca82f

1 file changed

Lines changed: 218 additions & 61 deletions

File tree

‎viewer-ohos/src/main/ets/render/SpreadsheetView.ets‎

Lines changed: 218 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { displaySync } from '@kit.ArkGraphics2D';
12
import * as model from '../core/OfficeModel';
23
import { RenderUtils } from './RenderUtils';
34

@@ -7,6 +8,23 @@ const GRID_COLOR = '#dce1e5';
78
const HEADER_BG = '#f3f5f7';
89
const HEADER_TEXT = '#52606d';
910

11+
/**
12+
* 预计算 0..255 列标字母,彻底消除表头绘制时的高频字符串循环拼接
13+
*/
14+
const COLUMN_LETTERS: string[] = (() => {
15+
const arr: string[] = new Array(256);
16+
for (let i = 0; i < 256; i++) {
17+
let letter = '';
18+
let temp = i;
19+
while (temp >= 0) {
20+
letter = String.fromCharCode((temp % 26) + 65) + letter;
21+
temp = Math.floor(temp / 26) - 1;
22+
}
23+
arr[i] = letter;
24+
}
25+
return arr;
26+
})();
27+
1028
/**
1129
* 轴前缀和与二分查找空间索引
1230
*/
@@ -67,8 +85,10 @@ class SheetMergeItem {
6785
* 1. 采用 rowStarts 稀疏索引与二分查找,仅遍历视口内可见单元格,彻底消除全表 O(N) 扫描
6886
* 2. 采用 Uint8Array 记录合并从属关系,消除每帧几十万次字符串分配与垃圾回收 (GC)
6987
* 3. 预先计算无缩放基础前缀和,重绘时零数组分配
88+
* 4. 采用 64 行分桶空间索引与代际去重,避免遍历全表千量级合并单元格
7089
*/
7190
class SheetLayoutData {
91+
static readonly BUCKET_ROW_SIZE = 64;
7292
readonly sheet: model.Sheet;
7393
readonly rows: SheetAxis;
7494
readonly columns: SheetAxis;
@@ -77,6 +97,9 @@ class SheetLayoutData {
7797
readonly rowStarts: Int32Array;
7898
readonly mergedCells: Uint8Array;
7999
readonly merges: SheetMergeItem[];
100+
readonly mergeBuckets: Int32Array[];
101+
readonly mergeSeenGen: Int32Array;
102+
currentGen: number = 0;
80103

81104
constructor(sheet: model.Sheet) {
82105
this.sheet = sheet;
@@ -123,6 +146,27 @@ class SheetLayoutData {
123146
this.merges.push(new SheetMergeItem(left, top, right, bottom, anchor));
124147
}
125148
}
149+
150+
const bucketCount = Math.max(1, Math.ceil(rowCount / SheetLayoutData.BUCKET_ROW_SIZE) + 1);
151+
const rawBuckets: number[][] = [];
152+
for (let b = 0; b < bucketCount; b++) {
153+
rawBuckets.push([]);
154+
}
155+
if (sheet.merges) {
156+
for (let mIdx = 0; mIdx < this.merges.length; mIdx++) {
157+
const m = sheet.merges[mIdx];
158+
const startB = Math.max(0, Math.min(bucketCount - 1, Math.floor(m.startRow / SheetLayoutData.BUCKET_ROW_SIZE)));
159+
const endB = Math.max(startB, Math.min(bucketCount - 1, Math.floor(m.endRow / SheetLayoutData.BUCKET_ROW_SIZE)));
160+
for (let b = startB; b <= endB; b++) {
161+
rawBuckets[b].push(mIdx);
162+
}
163+
}
164+
}
165+
this.mergeBuckets = new Array(bucketCount);
166+
for (let b = 0; b < bucketCount; b++) {
167+
this.mergeBuckets[b] = new Int32Array(rawBuckets[b]);
168+
}
169+
this.mergeSeenGen = new Int32Array(this.merges.length);
126170
}
127171

128172
// 二分查找:找到指定行中,列号 >= column 的第一个单元格下标
@@ -141,6 +185,13 @@ class SheetLayoutData {
141185
}
142186
}
143187

188+
interface CachedCellWrap {
189+
readonly scaleKey: number;
190+
readonly availWKey: number;
191+
readonly maxLines: number;
192+
readonly lines: string[];
193+
}
194+
144195
/**
145196
* XLSX 电子表格原生渲染组件:支持多工作表 Sheet 切换、视口虚拟化渲染、物理惯性滑动与手势缩放
146197
*/
@@ -175,12 +226,18 @@ export struct SpreadsheetView {
175226
private pinchStartOffsetX: number = 0;
176227
private pinchStartOffsetY: number = 0;
177228

178-
// 惯性滑动 (Fling) 物理仿真状态
229+
// 惯性滑动 (Fling) 物理仿真状态与硬件 VSync
179230
private flingTimer: number = -1;
180231
private retryTimer: number = -1;
181232
private flingVx: number = 0;
182233
private flingVy: number = 0;
183234
private lastFlingTime: number = 0;
235+
private vsync: displaySync.DisplaySync | null = null;
236+
private flingRunning: boolean = false;
237+
private lastVsyncTimestamp: number = 0;
238+
239+
// 单元格折行排版 WeakMap 缓存,避免平移滑动时重复进行 measureText 二分折行排版
240+
private readonly cellWrapCache: WeakMap<model.Cell, CachedCellWrap> = new WeakMap();
184241

185242
// 缓存当前 Sheet 的空间索引与几何布局(完全对齐安卓端的 SheetLayout)
186243
private cachedSheet: model.Sheet | null = null;
@@ -262,6 +319,14 @@ export struct SpreadsheetView {
262319
aboutToDisappear(): void {
263320
this.isReady = false;
264321
this.stopFling();
322+
if (this.vsync) {
323+
try {
324+
this.vsync.off('frame');
325+
} catch (_e) {
326+
// ignore
327+
}
328+
this.vsync = null;
329+
}
265330
if (this.retryTimer !== -1) {
266331
clearTimeout(this.retryTimer);
267332
this.retryTimer = -1;
@@ -271,6 +336,15 @@ export struct SpreadsheetView {
271336
}
272337

273338
private stopFling(): void {
339+
this.flingRunning = false;
340+
this.lastVsyncTimestamp = 0;
341+
if (this.vsync) {
342+
try {
343+
this.vsync.stop();
344+
} catch (_e) {
345+
// ignore
346+
}
347+
}
274348
if (this.flingTimer !== -1) {
275349
clearTimeout(this.flingTimer);
276350
this.flingTimer = -1;
@@ -290,15 +364,45 @@ export struct SpreadsheetView {
290364
}
291365
this.flingVx = vx;
292366
this.flingVy = vy;
367+
this.flingRunning = true;
368+
this.lastVsyncTimestamp = 0;
293369
this.lastFlingTime = Date.now();
294-
this.stepFling();
370+
371+
if (!this.vsync) {
372+
try {
373+
this.vsync = displaySync.create();
374+
this.vsync.on('frame', (frameInfo: displaySync.IntervalInfo) => {
375+
if (!this.flingRunning) return;
376+
const ts = frameInfo.timestamp;
377+
let dt = 0.016;
378+
if (this.lastVsyncTimestamp > 0 && ts > this.lastVsyncTimestamp) {
379+
dt = Math.min(0.032, Math.max(0.001, (ts - this.lastVsyncTimestamp) / 1e9));
380+
}
381+
this.lastVsyncTimestamp = ts;
382+
this.stepFlingWithDt(dt);
383+
});
384+
} catch (_e) {
385+
this.vsync = null;
386+
}
387+
}
388+
389+
if (this.vsync) {
390+
try {
391+
this.vsync.start();
392+
} catch (_e) {
393+
this.vsync = null;
394+
this.stepFlingFallback();
395+
}
396+
} else {
397+
this.stepFlingFallback();
398+
}
295399
}
296400

297-
private stepFling = (): void => {
298-
if (this.flingVx === 0 && this.flingVy === 0) return;
299-
const now = Date.now();
300-
const dt = Math.min(0.032, Math.max(0.001, (now - this.lastFlingTime) / 1000));
301-
this.lastFlingTime = now;
401+
private stepFlingWithDt(dt: number): void {
402+
if (!this.flingRunning || (this.flingVx === 0 && this.flingVy === 0)) {
403+
this.stopFling();
404+
return;
405+
}
302406

303407
// 指数阻尼衰减
304408
const friction = 2.8;
@@ -326,7 +430,17 @@ export struct SpreadsheetView {
326430
}
327431

328432
this.redraw();
329-
this.flingTimer = setTimeout(this.stepFling, 16);
433+
}
434+
435+
private stepFlingFallback = (): void => {
436+
if (!this.flingRunning) return;
437+
const now = Date.now();
438+
const dt = Math.min(0.032, Math.max(0.001, (now - this.lastFlingTime) / 1000));
439+
this.lastFlingTime = now;
440+
this.stepFlingWithDt(dt);
441+
if (this.flingRunning) {
442+
this.flingTimer = setTimeout(this.stepFlingFallback, 16);
443+
}
330444
};
331445

332446
private getCurrentSheet(): model.Sheet | undefined {
@@ -483,30 +597,57 @@ export struct SpreadsheetView {
483597
}
484598
ctx.stroke();
485599

486-
// 1.4 绘制可见的合并单元格(覆盖内部网格线并绘制锚点文本)
600+
// 1.4 绘制可见的合并单元格(覆盖内部网格线并绘制锚点文本,使用行分桶与代际去重)
487601
const visibleMerges: SheetMergeItem[] = [];
488-
for (const m of layout.merges) {
489-
if (m.right <= unscaledScrollX || m.left >= unscaledBodyRight ||
490-
m.bottom <= unscaledScrollY || m.top >= unscaledBodyBottom) {
491-
continue;
492-
}
493-
visibleMerges.push(m);
494-
const left = HEADER_WIDTH + ox + m.left * s;
495-
const top = HEADER_HEIGHT + oy + m.top * s;
496-
const right = HEADER_WIDTH + ox + m.right * s;
497-
const bottom = HEADER_HEIGHT + oy + m.bottom * s;
498-
const mergeW = right - left;
499-
const mergeH = bottom - top;
500-
if (mergeW <= 0 || mergeH <= 0) continue;
501-
502-
ctx.fillStyle = '#ffffff';
503-
ctx.fillRect(left, top, mergeW, mergeH);
504-
if (m.anchor) {
505-
this.drawSingleCell(ctx, m.anchor, left, top, mergeW, mergeH, s);
602+
layout.currentGen++;
603+
if (layout.currentGen >= 0x7FFFFFFF) {
604+
layout.currentGen = 1;
605+
layout.mergeSeenGen.fill(0);
606+
}
607+
const currentGen = layout.currentGen;
608+
609+
const startRow = firstRowIdx < rows.visible.length ? rows.visible[firstRowIdx] : 0;
610+
let endRow = startRow;
611+
const lastVisIdx = rows.first(unscaledBodyBottom);
612+
if (lastVisIdx < rows.visible.length) {
613+
endRow = rows.visible[lastVisIdx];
614+
} else if (rows.visible.length > 0) {
615+
endRow = rows.visible[rows.visible.length - 1];
616+
}
617+
const startB = Math.max(0, Math.min(layout.mergeBuckets.length - 1, Math.floor(startRow / SheetLayoutData.BUCKET_ROW_SIZE)));
618+
const endB = Math.max(startB, Math.min(layout.mergeBuckets.length - 1, Math.floor(endRow / SheetLayoutData.BUCKET_ROW_SIZE)));
619+
620+
for (let b = startB; b <= endB; b++) {
621+
const bucket = layout.mergeBuckets[b];
622+
for (let k = 0; k < bucket.length; k++) {
623+
const mIdx = bucket[k];
624+
if (layout.mergeSeenGen[mIdx] === currentGen) {
625+
continue;
626+
}
627+
layout.mergeSeenGen[mIdx] = currentGen;
628+
const m = layout.merges[mIdx];
629+
if (m.right <= unscaledScrollX || m.left >= unscaledBodyRight ||
630+
m.bottom <= unscaledScrollY || m.top >= unscaledBodyBottom) {
631+
continue;
632+
}
633+
visibleMerges.push(m);
634+
const left = HEADER_WIDTH + ox + m.left * s;
635+
const top = HEADER_HEIGHT + oy + m.top * s;
636+
const right = HEADER_WIDTH + ox + m.right * s;
637+
const bottom = HEADER_HEIGHT + oy + m.bottom * s;
638+
const mergeW = right - left;
639+
const mergeH = bottom - top;
640+
if (mergeW <= 0 || mergeH <= 0) continue;
641+
642+
ctx.fillStyle = '#ffffff';
643+
ctx.fillRect(left, top, mergeW, mergeH);
644+
if (m.anchor) {
645+
this.drawSingleCell(ctx, m.anchor, left, top, mergeW, mergeH, s);
646+
}
647+
ctx.strokeStyle = GRID_COLOR;
648+
ctx.lineWidth = 0.5;
649+
ctx.strokeRect(left, top, mergeW, mergeH);
506650
}
507-
ctx.strokeStyle = GRID_COLOR;
508-
ctx.lineWidth = 0.5;
509-
ctx.strokeRect(left, top, mergeW, mergeH);
510651
}
511652

512653
// 1.5 绘制可见单元格边框 (style.borders)
@@ -665,42 +806,55 @@ export struct SpreadsheetView {
665806
rawText = rawText.substring(0, end) + '\u2026';
666807
}
667808

668-
const finalLines: string[] = [];
809+
let finalLines: string[] = [];
669810
if (style?.wrap) {
670-
const paragraphs = rawText.split('\n');
671-
for (const p of paragraphs) {
672-
if (finalLines.length >= maxLines) break;
673-
if (p.length === 0) {
674-
finalLines.push('');
675-
continue;
676-
}
677-
let remaining = p;
678-
while (remaining.length > 0 && finalLines.length < maxLines) {
679-
const w = ctx.measureText(remaining).width;
680-
if (w <= availW) {
681-
finalLines.push(remaining);
682-
break;
811+
const scaleKey = Math.round(scale * 1000);
812+
const availWKey = Math.round(availW);
813+
const cached = this.cellWrapCache.get(cell);
814+
if (cached && cached.scaleKey === scaleKey && cached.availWKey === availWKey && cached.maxLines === maxLines) {
815+
finalLines = cached.lines;
816+
} else {
817+
const paragraphs = rawText.split('\n');
818+
for (const p of paragraphs) {
819+
if (finalLines.length >= maxLines) break;
820+
if (p.length === 0) {
821+
finalLines.push('');
822+
continue;
683823
}
684-
// 二分查找单行最大字符数
685-
let low = 1;
686-
let high = remaining.length;
687-
let best = 1;
688-
while (low <= high) {
689-
const mid = (low + high) >>> 1;
690-
if (ctx.measureText(remaining.substring(0, mid)).width <= availW) {
691-
best = mid;
692-
low = mid + 1;
693-
} else {
694-
high = mid - 1;
824+
let remaining = p;
825+
while (remaining.length > 0 && finalLines.length < maxLines) {
826+
const w = ctx.measureText(remaining).width;
827+
if (w <= availW) {
828+
finalLines.push(remaining);
829+
break;
695830
}
831+
// 二分查找单行最大字符数
832+
let low = 1;
833+
let high = remaining.length;
834+
let best = 1;
835+
while (low <= high) {
836+
const mid = (low + high) >>> 1;
837+
if (ctx.measureText(remaining.substring(0, mid)).width <= availW) {
838+
best = mid;
839+
low = mid + 1;
840+
} else {
841+
high = mid - 1;
842+
}
843+
}
844+
// 代理对保护:切到高代理时避免劈半产生非法 Unicode 字符
845+
if (best < remaining.length && remaining.charCodeAt(best - 1) >= 0xD800 && remaining.charCodeAt(best - 1) <= 0xDBFF) {
846+
best = best > 1 ? best - 1 : best + 1;
847+
}
848+
finalLines.push(remaining.substring(0, best));
849+
remaining = remaining.substring(best);
696850
}
697-
// 代理对保护:切到高代理时避免劈半产生非法 Unicode 字符
698-
if (best < remaining.length && remaining.charCodeAt(best - 1) >= 0xD800 && remaining.charCodeAt(best - 1) <= 0xDBFF) {
699-
best = best > 1 ? best - 1 : best + 1;
700-
}
701-
finalLines.push(remaining.substring(0, best));
702-
remaining = remaining.substring(best);
703851
}
852+
this.cellWrapCache.set(cell, {
853+
scaleKey,
854+
availWKey,
855+
maxLines,
856+
lines: finalLines
857+
});
704858
}
705859
} else {
706860
if (rawText.indexOf('\n') !== -1) {
@@ -793,6 +947,9 @@ export struct SpreadsheetView {
793947
}
794948

795949
private indexToColumnLetter(index: number): string {
950+
if (index >= 0 && index < COLUMN_LETTERS.length) {
951+
return COLUMN_LETTERS[index];
952+
}
796953
let letter = '';
797954
let temp = index;
798955
while (temp >= 0) {

0 commit comments

Comments
 (0)