Angular CDKで可変サイズや逆方向の仮想スクロールを実装するOSSを作った話
タイトルのままですが、 @rdlabo/ngx-cdk-scroll-strategies
をOSSとしてリリースしました。 Angular公式では
- 固定サイズの仮想スクロール:
FixedSizeVirtualScrollStrategy
- 可変サイズの仮想スクロール:
AutoSizeVirtualScrollStrategy
(Experimental)
のふたつが用意されています。固定サイズは(UI設計が固定サイズにできる場合は)問題なく使えるのですが、私の環境では可変サイズはスクロールがカクついたりジャンプしたりとうまく動かなかったんですよね。またソースコードを読んだところ、アイテムサイズを個別に持つのではなく、全体の平均サイズとして保持するようになっているので、大きなサイズのアイテムを削除したらスクロールがおかしくなる問題もあったり。
あと、どちらもLINEやWeChatのような逆スクロールはサポートされていませんでした。なので、プロダクトでは表示数を制限しながら数年間仮想スクロールなしで運用していたのですが、その制限をなくしたいと思って、 @rdlabo/ngx-cdk-scroll-strategies
を作成しました。
CdkDynamicSizeVirtualScroll
コンセプト: すべてが同一のアイテムサイズになる FixedSizeVirtualScrollStrategy
と、アイテムサイズが不明な AutoSizeVirtualScrollStrategy
の中間を狙って、「すべてのアイテムサイズを指定する」仮想スクロールです。コンセプトコードは以下の通りです。
<cdk-virtual-scroll-viewport [itemDynamicSizes]="[{ itemSize: 100 } , { itemSize: 80} , { itemSize: 90 } , { itemSize: 100}]">
<div *cdkVirtualFor="let item of [100, 80, 90, 100]; trackBy: trackByFn" [style.height.px]="item">
itemSize: {{ item }}
</div>
</cdk-virtual-scroll-viewport>
仮想スクロールのアイテム順と itemDynamicSizes
の順序は一致している必要があります。
ですので、別々にソートするなどするより、アイテムをSignalsで管理して、itemDynamicSizes
に渡す値はアイテムをComputedで変換するのがおすすめです。 @rx-angular/template
のように仮想スクロールのすべてを再実装するのではなく、 @angular/cdk
の CdkVirtualScrollViewport
に対してこのライブラリを追加することで、既存のコードに対して容易に導入できるようにしました。
デモ
以下で、「Simple」「Advanced」「Reverse」の3つのデモを用意しました。実際にさわって、開発者ツールからDOMの様子を観察してください。
インストール
% npm install @rdlabo/ngx-cdk-scroll-strategies
使い方
シンプルな使い方
一番シンプルな使い方です。Angular CDKの CdkVirtualScrollViewport
に対して、 CdkDynamicSizeVirtualScroll
を追加し、 itemDynamicSizes
にアイテムサイズを指定します。
- Demo: https://rdlabo-ionic-angular-library.netlify.app/main/scroll-strategies/simple
- Source: https://github.com/rdlabo-team/ionic-angular-library/blob/main/projects/demo/src/app/scroll-strategies/pages/scroll-simple
import { CdkDynamicSizeVirtualScroll, itemDynamicSize } from '@rdlabo/ngx-cdk-scroll-strategies';
@Component({
...
imports: [
CdkDynamicSizeVirtualScroll
],
})
export class ScrollStrategiesPage implements OnInit {
readonly items = signal<itemDynamicSize[]>([]);
readonly dynamicSize = computed<itemDynamicSize[]>(() => {
return this.items().map((item) => ({ trackId: item.trackId, itemSize: item.itemSize }));
});
}
<cdk-virtual-scroll-viewport [itemDynamicSizes]="dynamicSize()" minBufferPx="900" maxBufferPx="1350">
<div *cdkVirtualFor="let item of items(); trackBy: trackByFn" class="dynamic-item" [style.height.px]="item.itemSize">
itemSize: {{ item.itemSize }}
</div>
</cdk-virtual-scroll-viewport>
高度な使い方
ただシンプルな使い方では実プロダクトへの導入がしんどいので、高度なサンプルも用意しています。 CdkVirtualScrollViewport
ってよくできてて、切り出したコンポーネントを cdkVirtualFor
でループしていると、スクロール時にDOMを破棄せず、再利用してスクロールしたりするんですよね。なので、コンポーネント毎に実アイテムサイズを計測して取得する時は、親からではなくコンポーネント側で(Inputの値が変わるごとに)計測する必要があったりします。
また、都度アイテムサイズを反映すると計算量が多くなりすぎるので、「スクロールして、表示アイテムが変更されたタイミングで、新しい実測値があれば反映する等の工夫が必要です。
ほぼ私が実プロダクトで使ってるコードに準じています。流し読みするとしんどいので、レポジトリをローカルにcloneして、IDEの機能でジャンプしながら読む方がわかりやすいのではないかなと思います。Angularの基本的な知識は必要です。
- Demo: https://rdlabo-ionic-angular-library.netlify.app/main/scroll-strategies/advanced
- Source: https://github.com/rdlabo-team/ionic-angular-library/blob/main/projects/demo/src/app/scroll-strategies/pages/scroll-advanced
逆スクロールの使い方
LINEやWeChatのような逆スクロールで仮想スクロールを実装する方法です。これを実装する場合、必ず先にFlexboxの flex-direction: column-reverse;
と flex-direction: column;
を使ってスクロール計算を上下逆にする方法を学ぶようにしてください。以下のサンプルはとてもわかりやすいです。
本ディレクティブの逆スクロールはこれをベースに実装しています。
- Demo: https://rdlabo-ionic-angular-library.netlify.app/main/scroll-strategies/reverse
- Source: https://github.com/rdlabo-team/ionic-angular-library/blob/main/projects/demo/src/app/scroll-strategies/pages/scroll-reverse
逆スクロールを行う場合は、 isReverse
を true
に設定してください。また、逆スクロールの場合は、 cdkVirtualFor
でループさせるアイテムのラッパーDOM(以下では、 div.reverse-items
)を用意する必要があります。
<cdk-virtual-scroll-viewport [itemDynamicSizes]="dynamicSize()" [isReverse]="true" minBufferPx="900" maxBufferPx="1350">
<div class="reverse-items">
<div *cdkVirtualFor="let item of items(); trackBy: trackByFn" class="dynamic-item" [style.height.px]="item.itemSize">
itemSize: {{ item.itemSize }}
</div>
</div>
</cdk-virtual-scroll-viewport>
また、 cdk-virtual-scroll-viewport
に flex-direction: column-reverse;
を追加する必要があります。 isReverse
を true
にすると自動的に reverse-scroll
クラスが付与されるので、グローバルなCSS(styles.css等)に、以下を追加してください。
cdk-virtual-scroll-viewport {
width: 100%;
height: 100%;
// .reverse-scroll class is added from this directive.
&.reverse-scroll {
display: flex;
flex-direction: column-reverse;
.cdk-virtual-scroll-content-wrapper {
top: auto;
bottom: 0;
}
}
}
また、用意したラッパーDOM(ここでは、 div.reverse-items
)にもスタイルをあてる必要があります。
div.reverse-items {
height: 100%;
display: flex;
flex-direction: column-reverse;
position: relative;
bottom: 0;
}
まとめ
本ライブラリを使うと、 CdkVirtualScrollViewport
に対して、個別のアイテムサイズを指定することで、可変サイズや逆スクロールを実現することができるようになります。ぜひ、お試しください。
それでは、また。
Discussion