A way to lazy load images in <ion-slides>?


#1

I’ve built a simple photo gallery page which shows thumbnails of the photos in a grid. When the user clicks on a photo it presents a modal that shows the full-size image. The user can then swipe left or right to see the next images using .

My problem is that when the page containing the <ion-slides> is opened, it loads all of the images present in each <ion-slide>. This is not a problem with a handful of photos, however I could potentially have hundreds of photographs loading at the same time in no particular order resulting in a bad user experience.

Is there anyway I can change the behaviour so that only the current photo is loaded?

Modal markup:

<ion-header>
  <ion-navbar>
    <ion-title>{{ title | translate }}</ion-title>
    <ion-buttons end>
      <button ion-button (click)="dismiss()">{{ 'close' | translate }}</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
<ion-content>
  <ion-slides pager="true" [initialSlide]="initialSlide" paginationType="progress" zoom="true">
    <ion-slide *ngFor="let medium of media">
      <div class="swiper-zoom-container">
        <img [src]="medium.largeImage()">
      </div>
    </ion-slide>
  </ion-slides>
</ion-content>

Modal TS:

import { Component } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular';

// custom
import { Medium } from '../../models/medium';

@IonicPage()
@Component({
  selector: 'page-media-viewer',
  templateUrl: 'media-viewer.html',
})
export class MediaViewerPage {

  title: string;
  media: Medium[] = [];
  initialSlide: number;

  constructor(
    private viewCtrl: ViewController,
    private navParams: NavParams) {

    this.title = this.navParams.get('title');
    this.media = <Medium[]>this.navParams.get('media');
    let selected = <Medium>this.navParams.get('selected');
    this.setInitialSlide(selected);

  }

  dismiss() {
    this.viewCtrl.dismiss();
  }

  private setInitialSlide(medium: Medium) {
    let index = this.media.indexOf(medium);
    this.initialSlide = index;
  }

}

#2

Listen to slideChanged(). When the slide changes, get the current active index. Load the Observable that corresponds to your image at that index.


#3

Thanks @AaronSterling for pointers on how to accomplish this.

For anyone that’s interested, i ended up with the below solution. Not a particularly elegant solution, but does what I need it to do.

Markup:

<ion-header>
  <ion-navbar>
    <ion-title>{{ title | translate }}</ion-title>
    <ion-buttons end>
      <button ion-button navPop>{{ 'close' | translate }}</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
<ion-content>
  <ion-slides pager="true" [initialSlide]="initialSlide" paginationType="progress" zoom="true" (ionSlideReachStart)="firstSlide()" (ionSlideNextStart)="slideNextStart()" (ionSlidePrevStart)="slidePrevStart()">
    <ion-slide *ngFor="let medium of media">
      <div class="swiper-zoom-container" *ngIf="medium.viewed()">
        <img [src]="medium.largeImage()">
      </div>
    </ion-slide>
  </ion-slides>
</ion-content>

Modal code:

import { Component, ViewChild } from '@angular/core';
import { IonicPage, NavParams, Slides } from 'ionic-angular';

// Medium model
import { Medium } from '../../models/medium';

@IonicPage()
@Component({
  selector: 'page-media-viewer',
  templateUrl: 'media-viewer.html',
})
export class MediaViewerPage {

  private static readonly PRELOAD_SLIDES = 2;

  @ViewChild(Slides) slides: Slides;

  title: string;
  media: Medium[] = [];
  initialSlide: number;

  constructor(
    private navParams: NavParams) {

    this.title = this.navParams.get('title');
    this.media = <Medium[]>this.navParams.get('media');
    let selected = <Medium>this.navParams.get('selected');
    this.setInitialSlide(selected);

  }


  // Pre-load first n images if initial side index === 0
  firstSlide() {
    this.markViewed(0, true);
  }

  // Pre-load next n images if sliding forwards
  // Called when slides first initialized - but oddly, not if initial slide index === 0
  slideNextStart() {
    let currentIndex = this.slides.getActiveIndex();
    this.markViewed(currentIndex, true);
  }

  // Pre-load next n images if sliding backwards
  slidePrevStart() {
    let currentIndex = this.slides.getActiveIndex();
    this.markViewed(currentIndex, false);
  }

  // Set initial slide
  private setInitialSlide(medium: Medium) {
    let index = this.media.indexOf(medium);
    this.initialSlide = index;
  }

  // Mark next n images as viewed
  private markViewed(currentIndex: number, forward: boolean) {
    if (forward) {
      let offset = Math.min(currentIndex + MediaViewerPage.PRELOAD_SLIDES + 1, this.media.length);
      for(let i = currentIndex; i < offset; i++) {
        this.media[i].markViewed();
      }
    } else {
      let offset = Math.max(currentIndex - MediaViewerPage.PRELOAD_SLIDES - 1, 0);
      for(let i = currentIndex; i >= offset; i--) {
        this.media[i].markViewed();
      }
    }
  }

}

Medium model:

import { Deserializable } from './deserializable';
import * as AppConstants from '../app/app.constants';

export class Medium implements Deserializable<Medium> {

  public static readonly DEFAULT_IMAGE = AppConstants.processingAsset;

  id: number;
  small: string;
  medium: string;
  large: string;
  kind: string;
  video: string;
  duration: string;
  _viewed: boolean;

  constructor() {

  }

  deserialize(input: any): Medium {
    Object.assign(this, input);
    return this;
  }

  smallImage() {
    return this.small || Medium.DEFAULT_IMAGE;
  }

  mediumImage() {
    return this.medium || Medium.DEFAULT_IMAGE;
  }
  
  largeImage() {
    return this.large || Medium.DEFAULT_IMAGE;
  }

  canShowVideo() {
    return this.kind === 'video' && this.video;
  }

  canShowPhoto() {
    return this.kind === 'photo' || (this.kind === 'video' && this.video === undefined)
  }

  viewed() {
    return this._viewed || false;
  }

  markViewed() {
    if (!this.viewed()) {
      this._viewed = true;
    }
  }

}

#4

Another pretty smooth method using ng-lazyload-image:

<ion-slides pager class="item-images" #imageSlides>
	<ion-slide *ngFor="let imageUrl of item.imageUrls; let i = index">
		<img src="{{ imageUrl.url }}" (load)="item.imageLoaded=true" [width]="screenWidth" [ngClass]="{ 'item-img-default-size': !item.imageLoaded }"
		    *ngIf="i == 0">
		<img class="subitem-image" 
                       [src]="imagePlaceholder" 
                       [lazyLoad]="imageUrl.url" 
                       [scrollObservable]="imageSlides.ionSlideDrag"
                       [width]="screenWidth" 
		       *ngIf="i > 0" />
	</ion-slide>
</ion-slides>

NOTE: needed to load first image “normally” 'cause imageSlides.ionSlideDrag doesn’t fire until sliding begins.