ion-virtual-scroll

Contents

Virtual Scrollは、Virtualの "infinite" リストを表示します。 レコードの配列が、テンプレートを作成するデータを含むVirtual Scrollに渡されます。 各レコードに対して作成されるセルと呼ばれるテンプレートは、 アイテム、ヘッダー、およびフッターで構成できます。 パフォーマンス上の理由から、リスト内のすべてのレコードが一度にレンダリングされるわけではありません。 その代わりに、レコードの小さなサブセット(ビューポートをいっぱいにするのに十分な数)がレンダリングされ、ユーザーのスクロールに再利用されます。

Approximate Widths and Heights

仮想スクロールのアイテムの高さがデフォルトサイズの 40px に近くない場合は、 approxItemHeight プロパティの値を指定することが非常に重要です。 ピクセル単位の正確なサイズは必要ありませんが、見積もりなしでは仮想スクロールは正しくレンダリングされません。

各テンプレートのおおよその幅と高さを使用して、 作成するセルの数を決定したり、 スクロール可能領域の高さを計算したりします。 各セルの実際の描画サイズはアプリケーションのCSSから得られるのに対し、 この近似値は初期次元の計算にのみ使用されることに注意してください。

また、Ionicのデフォルトのアイテムのサイズは、 プラットフォームによって高さが若干異なりますが、これは問題ありません。

Images Within Virtual Scroll

HTTPリクエスト、イメージのデコード、およびイメージのレンダリングによって、 スクロール中にjankが発生することがあります。 Ionicはイメージをより良く制御するために、 HTTPリクエストとイメージレンダリングを管理する <ion-img> を提供している。 アイテムをすばやくスクロールすると、<ion-img> はいつ要求を行わないか、 いつ画像をレンダリングしないかを認識し、スクロール後に表示可能な画像のみをロードします。 詳細については、 Read more about ion-img. を参照してください。

また、アプリ開発者にとって重要なのは、画像サイズがロックされていることを確認することであり、 画像が完全にロードされた後、サイズが変更されたり、 他の要素サイズに影響を与えたりすることはないことに注意ください。 簡単に言えば、レンダリングバグが発生しないようにするには、 仮想アイテム内の要素が動的に変化しないことが重要です。

Virtual Scrollでは、<img> のNatural Effectsは望ましくありません。 DOMに要素を追加すると、<img> 要素は直ち画像ファイルに対するHTTP要求を作成するため、 ネイティブの <img> 要素よりも <ion-img> コンポーネントを使用することをお薦めします。 さらに、<img>は、ユーザーがスクロールしている間であれば常にレンダリングされてしまいます。 <ion-img> は、含有する ion-content によって制御され、 高速にスクロールしてもイメージをレンダリングしません。

Virtual Scroll Performance Tips

iOS Cordova WKWebView

Cordovaを使ってiOSにデプロイする場合は、 WKWebView plugin プラグインを使ってiOSの高性能なウェブビューを利用することを強くお勧めします。 さらに、WKWebViewは、以前のUIWebViewよりも スクロールの効率が優れています。

要素のサイズと位置を固定する

仮想スクロールですべてのアイテムのサイズと位置を効率的に変更するには、 各仮想アイテム内のすべての要素がそのサイズや位置を 動的に変更しないことが非常に重要です。 サイズと位置が変わらないようにする最善の方法は、 各仮想アイテムがCSSを使ってそのサイズにロックされていることです。

画像に ion-img を使う

仮想スクロールにイメージを含める場合は、標準のHTML要素ではなく、 必ずion-img要素を使用してください。 ion-imgでは、画像は遅延ロードされるため、表示可能な画像のみがレンダリングされ、 HTTPリクエストはスクロール中に効率的に制御されます。

概算の幅と高さを設定する

前述のように、すべての要素の寸法がロックされます。 ただし、virtual scrollは、レンダリングされるまで寸法を認識しません。 最初のレンダリングでは、 virtual scrollで構築する項目数を設定する必要があります。 approxItemHeight などの"approx"プロパティを使用すると、 virtual scrollにおおよそのサイズを指定できるため、 virtual scrollで作成する項目数を決定できます。

データセットの変更にはvirtualTrackByを使用する

データは変更されていなくても、イテレータ内の要素のIDは変更されます。 たとえば、RPCからサーバーに対してイテレーターが生成され、 そのRPCが再実行された場合などです。 「データ」が変更されていなくても、2回目の応答では異なるIDを持つオブジェクトが生成され、 IonicはDOM全体を分解して再構築します。 これはハイコストな操作であり、可能であれば回避する必要があります。

効率的なヘッダーおよびフッター機能

各仮想アイテムは非常に効率的でなければなりませんが、実際にそのパフォーマンスを低下させる1つの方法は、 セクション・ヘッダーおよびフッター関数内でDOM操作を実行することです。 これらの関数はデータセット内のすべてのレコードに対して呼び出されるため、 パフォーマンスコストが高いことを確認してください。

利用方法

<ion-content>
  <ion-virtual-scroll [items]="items" approxItemHeight="320px">
    <ion-card *virtualItem="let item; let itemBounds = bounds;">
      <div>
        <ion-img [src]="item.imgSrc" [height]="item.imgHeight" [alt]="item.name"></ion-img>
      </div>
    <ion-card-header>
      <ion-card-title>{{ item.name }}</ion-card-title>
    </ion-card-header>
    <ion-card-content>{{ item.content }}</ion-card-content>
    </ion-card>
  </ion-virtual-scroll>
</ion-content>
export class VirtualScrollPageComponent {
  items: any[] = [];

  constructor() {
    for (let i = 0; i < 1000; i++) {
      this.items.push({
        name: i + ' - ' + images[rotateImg],
        imgSrc: getImgSrc(),
        avatarSrc: getImgSrc(),
        imgHeight: Math.floor(Math.random() * 50 + 150),
        content: lorem.substring(0, Math.random() * (lorem.length - 100) + 100)
      });

      rotateImg++;
      if (rotateImg === images.length) {
        rotateImg = 0;
      }
    }
  }
}

const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, seddo eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';

const images = [
  'bandit',
  'batmobile',
  'blues-brothers',
  'bueller',
  'delorean',
  'eleanor',
  'general-lee',
  'ghostbusters',
  'knight-rider',
  'mirth-mobile'
];

function getImgSrc() {
  const src = 'https://dummyimage.com/600x400/${Math.round( Math.random() * 99999)}/fff.png';
  rotateImg++;
  if (rotateImg === images.length) {
    rotateImg = 0;
  }
  return src;
}

let rotateImg = 0;

Basic

The array of records should be passed to the items property on the ion-virtual-scroll element. The data given to the items property must be an array. An item template with the *virtualItem property is required in the ion-virtual-scroll. The *virtualItem property can be added to any element.

<ion-virtual-scroll [items]="items">
  <ion-item *virtualItem="let item">
    {{ item }}
  </ion-item>
</ion-virtual-scroll>

Section Headers and Footers

Section headers and footers are optional. They can be dynamically created from developer-defined functions. For example, a large list of contacts usually has a divider for each letter in the alphabet. Developers provide their own custom function to be called on each record. The logic in the custom function should determine whether to create the section template and what data to provide to the template. The custom function should return null if a template shouldn't be created.

<ion-virtual-scroll [items]="items" [headerFn]="myHeaderFn">
  <ion-item-divider *virtualHeader="let header">
    {{ header }}
  </ion-item-divider>
  <ion-item *virtualItem="let item">
    Item: {{ item }}
  </ion-item>
</ion-virtual-scroll>

Below is an example of a custom function called on every record. It gets passed the individual record, the record's index number, and the entire array of records. In this example, after every 20 records a header will be inserted. So between the 19th and 20th records, between the 39th and 40th, and so on, a <ion-item-divider> will be created and the template's data will come from the function's returned data.

myHeaderFn(record, recordIndex, records) {
  if (recordIndex % 20 === 0) {
    return 'Header ' + recordIndex;
  }
  return null;
}

Custom Components

If a custom component is going to be used within Virtual Scroll, it's best to wrap it with a <div> to ensure the component is rendered correctly. Since each custom component's implementation and internals can be quite different, wrapping within a <div> is a safe way to make sure dimensions are measured correctly.

<ion-virtual-scroll [items]="items">
  <div *virtualItem="let item">
    <my-custom-item [item]="item">
      {{ item }}
    </my-custom-item>
  </div>
</ion-virtual-scroll>

プロパティ

approxFooterHeight

Description

The approximate width of each footer template's cell. This dimension is used to help determine how many cells should be created when initialized, and to help calculate the height of the scrollable area. This height value can only use px units. Note that the actual rendered size of each cell comes from the app's CSS, whereas this approximation is used to help calculate initial dimensions before the item has been rendered.

Attributeapprox-footer-height
Typenumber
Default30

approxHeaderHeight

Description

The approximate height of each header template's cell. This dimension is used to help determine how many cells should be created when initialized, and to help calculate the height of the scrollable area. This height value can only use px units. Note that the actual rendered size of each cell comes from the app's CSS, whereas this approximation is used to help calculate initial dimensions before the item has been rendered.

Attributeapprox-header-height
Typenumber
Default30

approxItemHeight

Description

It is important to provide this if virtual item height will be significantly larger than the default The approximate height of each virtual item template's cell. This dimension is used to help determine how many cells should be created when initialized, and to help calculate the height of the scrollable area. This height value can only use px units. Note that the actual rendered size of each cell comes from the app's CSS, whereas this approximation is used to help calculate initial dimensions before the item has been rendered.

Attributeapprox-item-height
Typenumber
Default45

footerFn

Description

Section footers and the data used within its given template can be dynamically created by passing a function to footerFn. The logic within the footer function can decide if the footer template should be used, and what data to give to the footer template. The function must return null if a footer cell shouldn't be created.

Type((item: any, index: number, items: any[]) => string | null | undefined) | undefined

footerHeight

Description

An optional function that maps each item footer within their height.

Type((item: any, index: number) => number) | undefined

headerFn

Description

Section headers and the data used within its given template can be dynamically created by passing a function to headerFn. For example, a large list of contacts usually has dividers between each letter in the alphabet. App's can provide their own custom headerFn which is called with each record within the dataset. The logic within the header function can decide if the header template should be used, and what data to give to the header template. The function must return null if a header cell shouldn't be created.

Type((item: any, index: number, items: any[]) => string | null | undefined) | undefined

headerHeight

Description

An optional function that maps each item header within their height.

Type((item: any, index: number) => number) | undefined

itemHeight

Description

An optional function that maps each item within their height. When this function is provides, heavy optimizations and fast path can be taked by ion-virtual-scroll leading to massive performance improvements.

This function allows to skip all DOM reads, which can be Doing so leads to massive performance

Type((item: any, index: number) => number) | undefined

items

Description

The data that builds the templates within the virtual scroll. It's important to note that when this data has changed, then the entire virtual scroll is reset, which is an expensive operation and should be avoided if possible.

Typeany[] | undefined

nodeRender

Description

NOTE: only Vanilla JS API.

Type((el: HTMLElement | null, cell: Cell, domIndex: number) => HTMLElement) | undefined

renderFooter

Description

NOTE: only JSX API for stencil.

Provide a render function for the footer to be rendered. Returns a JSX virtual-dom.

Type((item: any, index: number) => any) | undefined

renderHeader

Description

NOTE: only JSX API for stencil.

Provide a render function for the header to be rendered. Returns a JSX virtual-dom.

Type((item: any, index: number) => any) | undefined

renderItem

Description

NOTE: only JSX API for stencil.

Provide a render function for the items to be rendered. Returns a JSX virtual-dom.

Type((item: any, index: number) => any) | undefined

メソッド

checkEnd

Description

This method marks the tail the items array as dirty, so they can be re-rendered.

It's equivalent to calling:

virtualScroll.checkRange(lastItemLen);
SignaturecheckEnd() => Promise<void>

checkRange

Description

This method marks a subset of items as dirty, so they can be re-rendered. Items should be marked as dirty any time the content or their style changes.

The subset of items to be updated can are specifing by an offset and a length.

SignaturecheckRange(offset: number, len?: number) => Promise<void>

positionForItem

Description

Returns the position of the virtual item at the given index.

SignaturepositionForItem(index: number) => Promise<number>