如何在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
相关产品推荐
相关产品推荐

