Skip to content

Commit b3b5bbb

Browse files
authored
🎨 Improve automatic scrolling when dragging items in the outline (#15846)
1 parent 950f392 commit b3b5bbb

File tree

1 file changed

+73
-4
lines changed

1 file changed

+73
-4
lines changed

app/src/layout/dock/Outline.ts

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export class Outline extends Model {
3636
public blockId: string;
3737
public isPreview: boolean;
3838
private preFilterExpandIds: string[] | null = null;
39+
private scrollAnimationId: number | null = null;
40+
private scrollLastFrameTime: number = 0;
41+
private scrollCurrentFPS: number = 60;
3942

4043
constructor(options: {
4144
app: App,
@@ -374,11 +377,68 @@ export class Outline extends Model {
374377
});
375378
return;
376379
}
380+
// 检查是否在滚动边界区域
377381
if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) {
378-
this.element.scroll({
379-
top: this.element.scrollTop + (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -Constants.SIZE_SCROLL_STEP : Constants.SIZE_SCROLL_STEP),
380-
behavior: "smooth"
381-
});
382+
// 如果还没有开始滚动,则开始持续滚动
383+
if (!this.scrollAnimationId) {
384+
const scrollDirection = moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -1 : 1;
385+
this.scrollLastFrameTime = performance.now();
386+
let scrollFrameCount = 0;
387+
388+
const scrollAnimation = (currentTime: number) => {
389+
if (!this.scrollAnimationId) {
390+
return;
391+
}
392+
393+
// 每隔 20 帧重新计算一次帧率
394+
if (scrollFrameCount % 20 === 0) {
395+
const deltaTime = currentTime - this.scrollLastFrameTime;
396+
this.scrollLastFrameTime = currentTime;
397+
// 计算过去 20 帧的平均帧率
398+
this.scrollCurrentFPS = deltaTime > 0 ? (20 * 1000) / deltaTime : 60;
399+
}
400+
scrollFrameCount++;
401+
402+
// 基于当前帧率计算滚动步长,确保等效于 60fps 时的 16px/帧
403+
const baseScrollStep = 16;
404+
const targetFPS = 60;
405+
const scrollStep = baseScrollStep * (targetFPS / this.scrollCurrentFPS);
406+
407+
this.element.scroll({
408+
top: this.element.scrollTop + scrollStep * scrollDirection
409+
});
410+
411+
// 使用 requestAnimationFrame 继续动画
412+
this.scrollAnimationId = requestAnimationFrame(scrollAnimation);
413+
};
414+
415+
// 检查浏览器是否支持 requestAnimationFrame
416+
if (typeof requestAnimationFrame !== "undefined") {
417+
this.scrollAnimationId = requestAnimationFrame(scrollAnimation);
418+
} else {
419+
// 回退到 setTimeout 方法
420+
const scrollInterval = 16; // 约 60fps
421+
const scrollStep = 16; // 每次滚动的距离
422+
423+
const scrollAnimationFallback = () => {
424+
this.element.scroll({
425+
top: this.element.scrollTop + scrollStep * scrollDirection
426+
});
427+
this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval);
428+
};
429+
this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval);
430+
}
431+
}
432+
} else {
433+
// 离开滚动区域时停止滚动
434+
if (this.scrollAnimationId) {
435+
if (typeof cancelAnimationFrame !== "undefined") {
436+
cancelAnimationFrame(this.scrollAnimationId);
437+
} else {
438+
clearTimeout(this.scrollAnimationId);
439+
}
440+
this.scrollAnimationId = null;
441+
}
382442
}
383443
selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement;
384444
if (!selectItem || selectItem.tagName !== "LI" || selectItem.style.position === "fixed") {
@@ -410,6 +470,15 @@ export class Outline extends Model {
410470
documentSelf.onselect = null;
411471
ghostElement?.remove();
412472
item.style.opacity = "";
473+
// 清理滚动动画
474+
if (this.scrollAnimationId) {
475+
if (typeof cancelAnimationFrame !== "undefined") {
476+
cancelAnimationFrame(this.scrollAnimationId);
477+
} else {
478+
clearTimeout(this.scrollAnimationId);
479+
}
480+
this.scrollAnimationId = null;
481+
}
413482
if (!selectItem) {
414483
selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover");
415484
}

0 commit comments

Comments
 (0)