You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在CustomVirtualStrategy中计算itemHeights?解决聊天滚动偏差

解决CDK虚拟滚动动态高度消息的滚动定位问题

核心思路:动态跟踪消息项实际高度并更新滚动策略

由于消息项使用多行textarea导致高度不固定,默认虚拟滚动依赖固定itemHeight的计算逻辑会失效,进而出现滚动位置偏上的问题。解决关键在于实时获取每个消息的实际高度,让虚拟滚动视图基于真实高度计算滚动位置。

步骤1:实现自定义虚拟滚动策略

基于Angular CDK的BaseVirtualScrollStrategy扩展自定义策略,用于维护和计算动态高度:

import { Injectable } from '@angular/core';
import { BaseVirtualScrollStrategy, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';

@Injectable()
export class DynamicHeightScrollStrategy extends BaseVirtualScrollStrategy {
  private itemHeights: number[] = [];
  private totalContentHeight = 0;

  attach(viewport: CdkVirtualScrollViewport): void {
    super.attach(viewport);
    // 监听滚动,更新可见项高度
    viewport.elementScrolled().subscribe(() => this.updateVisibleItemHeights());
  }

  // 更新当前可见消息的高度
  updateVisibleItemHeights(): void {
    const renderedItems = this.viewport?.getRenderedItems();
    if (!renderedItems) return;

    renderedItems.forEach((item, idx) => {
      const actualIndex = this.viewport.getRenderedRange().start + idx;
      const element = item.element as HTMLElement;
      const currentHeight = element.offsetHeight;

      if (this.itemHeights[actualIndex] !== currentHeight) {
        this.itemHeights[actualIndex] = currentHeight;
        this.recalculateTotalHeight();
        // 通知视图重新计算滚动范围
        this.viewport.checkViewportSize();
      }
    });
  }

  // 计算所有消息的总高度
  private recalculateTotalHeight(): void {
    this.totalContentHeight = this.itemHeights.reduce((sum, height) => sum + height, 0);
  }

  // 自定义滚动到底部方法
  scrollToBottom(): void {
    this.updateVisibleItemHeights();
    if (!this.viewport) return;
    const viewportHeight = this.viewport.getViewportSize();
    this.viewport.scrollToPosition(this.totalContentHeight - viewportHeight);
  }

  // 重写按索引滚动的逻辑(可选)
  scrollToIndex(index: number, behavior?: ScrollBehavior): void {
    this.updateVisibleItemHeights();
    const scrollPosition = this.itemHeights.slice(0, index).reduce((sum, h) => sum + h, 0);
    this.viewport?.scrollToPosition(scrollPosition, behavior);
  }
}

步骤2:在聊天组件中使用自定义策略

在组件中注入并绑定自定义策略,替代默认的滚动逻辑:

import { Component, ViewChild, AfterViewChecked } from '@angular/core';
import { CdkVirtualScrollViewport, VirtualScrollStrategy } from '@angular/cdk/scrolling';
import { DynamicHeightScrollStrategy } from './dynamic-height-scroll-strategy';

@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  providers: [{ provide: VirtualScrollStrategy, useClass: DynamicHeightScrollStrategy }]
})
export class ChatComponent implements AfterViewChecked {
  messages: any[] = [];
  private needScrollToBottom = false;

  @ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport;
  constructor(private scrollStrategy: DynamicHeightScrollStrategy) {}

  onNewMessage(message: any): void {
    this.messages.push(message);
    this.needScrollToBottom = true;
  }

  ngAfterViewChecked(): void {
    // DOM更新完成后执行滚动,避免setTimeout
    if (this.needScrollToBottom) {
      this.scrollStrategy.scrollToBottom();
      this.needScrollToBottom = false;
    }
  }
}

步骤3:优化高度计算效率

  • 缓存已渲染消息的高度:对于内容不会变化的历史消息,首次计算后存储高度,后续无需重复计算
  • 限制高度更新时机:仅在新消息添加、窗口 resize 或滚动时触发高度更新,避免频繁计算

内容的提问来源于stack exchange,提问作者Sunstrike527

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 17:40:43