From aa543888ec12f9a5c13b845aeef66b858e775df9 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:09:34 +0800 Subject: [PATCH] :art: Improve automatic scrolling when dragging items in the outline --- app/src/layout/dock/Outline.ts | 77 ++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/app/src/layout/dock/Outline.ts b/app/src/layout/dock/Outline.ts index 999cdb48142..4ffa7453524 100644 --- a/app/src/layout/dock/Outline.ts +++ b/app/src/layout/dock/Outline.ts @@ -26,6 +26,9 @@ export class Outline extends Model { public blockId: string; public isPreview: boolean; private openNodes: { [key: string]: string[] } = {}; + private scrollAnimationId: number | null = null; + private scrollLastFrameTime: number = 0; + private scrollCurrentFPS: number = 60; constructor(options: { app: App, @@ -256,11 +259,68 @@ export class Outline extends Model { }); return; } + // 检查是否在滚动边界区域 if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) { - this.element.scroll({ - top: this.element.scrollTop + (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -Constants.SIZE_SCROLL_STEP : Constants.SIZE_SCROLL_STEP), - behavior: "smooth" - }); + // 如果还没有开始滚动,则开始持续滚动 + if (!this.scrollAnimationId) { + const scrollDirection = moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -1 : 1; + this.scrollLastFrameTime = performance.now(); + let scrollFrameCount = 0; + + const scrollAnimation = (currentTime: number) => { + if (!this.scrollAnimationId) { + return; + } + + // 每隔 20 帧重新计算一次帧率 + if (scrollFrameCount % 20 === 0) { + const deltaTime = currentTime - this.scrollLastFrameTime; + this.scrollLastFrameTime = currentTime; + // 计算过去 20 帧的平均帧率 + this.scrollCurrentFPS = deltaTime > 0 ? (20 * 1000) / deltaTime : 60; + } + scrollFrameCount++; + + // 基于当前帧率计算滚动步长,确保等效于 60fps 时的 16px/帧 + const baseScrollStep = 16; + const targetFPS = 60; + const scrollStep = baseScrollStep * (targetFPS / this.scrollCurrentFPS); + + this.element.scroll({ + top: this.element.scrollTop + scrollStep * scrollDirection + }); + + // 使用 requestAnimationFrame 继续动画 + this.scrollAnimationId = requestAnimationFrame(scrollAnimation); + }; + + // 检查浏览器是否支持 requestAnimationFrame + if (typeof requestAnimationFrame !== "undefined") { + this.scrollAnimationId = requestAnimationFrame(scrollAnimation); + } else { + // 回退到 setTimeout 方法 + const scrollInterval = 16; // 约 60fps + const scrollStep = 16; // 每次滚动的距离 + + const scrollAnimationFallback = () => { + this.element.scroll({ + top: this.element.scrollTop + scrollStep * scrollDirection + }); + this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval); + }; + this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval); + } + } + } else { + // 离开滚动区域时停止滚动 + if (this.scrollAnimationId) { + if (typeof cancelAnimationFrame !== "undefined") { + cancelAnimationFrame(this.scrollAnimationId); + } else { + clearTimeout(this.scrollAnimationId); + } + this.scrollAnimationId = null; + } } selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement; if (!selectItem || selectItem.tagName !== "LI" || selectItem.style.position === "fixed") { @@ -292,6 +352,15 @@ export class Outline extends Model { documentSelf.onselect = null; ghostElement?.remove(); item.style.opacity = ""; + // 清理滚动动画 + if (this.scrollAnimationId) { + if (typeof cancelAnimationFrame !== "undefined") { + cancelAnimationFrame(this.scrollAnimationId); + } else { + clearTimeout(this.scrollAnimationId); + } + this.scrollAnimationId = null; + } if (!selectItem) { selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover"); }