Mosaic image gallery with Sencha Touch 2.2

Swarnendu De April 22, 2013

Few days ago I was searching for a mobile image gallery with sencha and I found a great extension for jQuery Mobile at PhotoSwipe . It seems almost all the apps developed with jQuery Mobile and with an Image gallery are actually using this extension for the gallery.

However, while searched for the same with Sencha Touch, I hardly found something readily available. A number of  posts are there in the Sencha Touch forum asking for a ready-made component – but there isn’t available till now. So, here is one simple photo gallery with a tweak which I used lately for one of our products.

[button link=”https://www.innofied.com/iphonetest/?url=https://www.innofied.com/blog/senchagallery” linking=”new-window” size=”medium” type=”simple” title=”Mosaic Image Gallery with Sencha Touch”]Demo[/button]

[button link=”https://github.com/innofied/senchagallery” linking=”new-window” size=”medium” type=”simple” title=”Download” label=”Download”]Download[/button]

gallery

 

Let’s start with creating a Sencha Touch project. There are mostly two views – one panel to show the thumbnails and the other is a Carousel to show full view of the images. I have used a Sencha Container to show thumbnails with template. However, you can use a Dataview if necessary but in that case you need to change the css a bit.

For the images, we  simply use Flickr image feed api. So, the Gallery,js file looks like this:

Gallery.js

Ext.define('SenchaGallery.view.Gallery', {
  extend: 'Ext.Container',
  xtype: 'gallery',
  requires: ['Ext.data.JsonP'],
  config: {
    cls: 'gallery',
    scrollable: true,
    // Template to show the thumbnail images
    tpl: Ext.create('Ext.XTemplate',
      '<tpl if="this.isEmpty(items)">',
      '<div class="empty-text empty-gallery">No image available</div>',
      '</tpl>',
      '<div class="gallery" id="photos">',
      '<tpl for="items">',
      '<img src="{media.m:this.getThumbnail}" class="thumbnail" data-fullimage="{media.m:this.getFullImage}"/>',
      '</tpl>',
      '</div>', {
      isEmpty: function (items) {
        if (items.length === 0) {
          return true;
        }

        return false;
      },

      getThumbnail: function (url) {
        return url.replace('_m', '_t');
      },

      getFullImage: function (url) {
        return url.replace('_m', '_n');
      }
    })
  },

If there is no image, we will show a message. Flickr api provides multiple sizes of every image (check it here). The public feed provides only “_m” size images – hence template functions getThumbnail and getFullImage provide “_t” (i.e. thumbnail) and “_n” (i.e. mobile image sizes) urls.

We add a loadImages method in Gallery view which fetches the feed data from Flickr API and add the images.

loadImages()

/**
 * Load the images and add them to the gallery container
 * Here is the point where you have to change the fetching mechanism 
 * say to get data with proxy and save in a Store.
 * Also, you may have to change the   
 */
loadImages: function () {
  var me = this;

  Ext.data.JsonP.request({
    url: 'http://api.flickr.com/services/feeds/photos_public.gne',
    callbackKey: 'jsoncallback',
    params: {
      format: 'json'
    },
    success: function (response) {
      me.items = response.items;
      me.setData(response);
    }
  });
}

Now, lets call this loadImages from initialize method.

initialize()

initialize: function () {
  var me = this;

  // Add tap event on the images to open the carousel
  me.element.on('tap', function (e, el) {
    me.showGalleryCarousel(el);
  }, me, {
    delegate: 'img.thumbnail'
  });

  me.loadImages();
  me.callParent(arguments);
}

And here we get the gallery:

gallery_3

To get a mosaic layout, we need to add -webkit-column-count to the parent container’s style.

#photos {
    /* Prevent vertical gaps */
    line-height: 0;
    -webkit-column-count: 3;
    -webkit-column-gap: 0px;
    margin: 10px 10px 0 15px;
}

#photos img {
    width: 95% !important;
    height: auto !important;
    margin: 0px 0px 10px 0px;
    border: 2px solid rgba(0,0,0,0.4);
    border-radius: 5px;
    padding: 5px;
    background: rgba(0,0,0,0.5);
}

 

And here is what we get:

gallery_1

So, we are done with the first view displaying all the image thumbnails as mosaic layout. We need to handle image tap event but before that, lets create a Gallery Carousel panel to show the full screen images.

GalleryCarousel.js

Ext.define('SenchaGallery.view.GalleryCarousel', {
  extend: 'Ext.carousel.Carousel',
  xtype: 'gallerycarousel',
  requires: ['Ext.TitleBar'],
  config: {
    fullscreen: true,
    modal: true,
    images: [],
    html: '<div class="close-gallery" data-action="close_carousel"></div>',
    cls: 'gallery-carousel',
    showAnimation: 'popIn',
    hideAnimation: 'popOut',
    indicator: false,
    listeners: {
      // Call image count checker for each image change
      activeitemchange: function (carousel, newPanel) {
        this.changeImageCount(newPanel);
      }
    }
  },

  initialize: function () {
    var me = this;

    // Create a bottom bar which will show the image count
    me.bottomBar = Ext.create('Ext.TitleBar', {
      xtype: 'titlebar',
      baseCls: Ext.baseCSSPrefix + 'infobar',
      name: 'info_bar',
      title: '',
      docked: 'bottom',
      cls: 'gallery-bottombar',
      items: [{
          xtype: 'button',
          align: 'left',
          iconCls: 'nav gallery-left',
          ui: 'plain',
          handler: function () {
            me.previous();
          }
        }, {
          xtype: 'button',
          align: 'right',
          iconCls: 'nav gallery-right',
          ui: 'plain',
          handler: function () {
            me.next();
          }
        }
      ]
    });

    // Add the images as separate containers in the carousel
    for (var i = 0; i < me.getImages().length; i++) {
      me.add({
        xtype: 'container',
        html: '<img class="gallery-item" src="' + Ext.get(me.getImages()[i]).getAttribute('data-fullimage') + '" />',
        index: i + 1
      });
    }

    me.add(me.bottomBar);
    me.callParent(arguments);
  },

  /**
   * Change image count at bottom bar
   */
  changeImageCount: function (activePanel) {
    var me = this;
    me.bottomBar.setTitle(activePanel.config.index + ' of ' + me.getImages().length);
  }
});

 

While creating an instance of this carousel, we will need to send all the images as an array to this carousel as config item. Inside initialize function, for each image create a container and add a “index” property to be used at the bottom bar. We add a listener for “activeitemchange” which fires every time we change one image to another and subsequently it updates the bottom bar title with the current image index.

Css for this carousel goes here:

/*********************** Gallery Carousel *************************/

.gallery-carousel {
    z-index: 20;
    background: rgba(0,0,0,0.7);
}

.gallery-item {
    max-width: 100%;
}

.gallery-carousel .x-carousel-item .x-innerhtml {
    height: 100% !important;
    display: -webkit-box;
    -webkit-box-align: center;
    -webkit-box-pack: center;
}

.gallery-carousel .x-carousel-indicator-dark span {
    background-color: rgba(255,255,255,0.3) !important;
}

.gallery-carousel .x-carousel-indicator-dark span.x-carousel-indicator-active {
    background-color: rgba(255,255,255,1) !important;
}

.gallery-carousel .close-gallery {
    position: absolute;
    -webkit-mask-image: url(../images/close.png);
    -webkit-mask-repeat: no-repeat;
    background: #fff;
    -webkit-mask-size: 32px;
    height: 32px;
    width: 32px;
    margin: 10px;
    right: 0;
}

.gallery-carousel .x-carousel-item .x-innerhtml {
    text-align: center;
}

.gallery-carousel .item {
    max-width: 100%;
    max-height: 100%;
}

.gallery-carousel .nav {
    -webkit-mask: url(../images/arrow.png) no-repeat;
    background: #fff;
    min-width: 16px;
    min-height: 16px;
    -webkit-mask-position: 12px 6px;
}

.gallery-carousel .gallery-left {
    -webkit-transform: rotate(180deg);
}

.gallery-carousel .gallery-right {
}

.gallery-carousel .gallery-bottombar .x-title {
    font-size: 14px !important;
    color: #fff;
}


/*********************** Gallery Carousel ENDS *************************/

Now the only task pending is to open this carousel while user taps on an image. For this, lets add a “showGalleryCarousel” function in Gallery.js.

showGalleryCarousel()

/**
 * Show the gallery carousel with all the images
 */
showGalleryCarousel: function (clickedImage) {
  var me = this,
    clickedImgIndex = 0;

  // Query all the images and save in an array
  me.images = me.images || me.element.query('img.thumbnail');

  // Create the Gallery Carousel
  var galleryCarousel = Ext.Viewport.add({
    xtype: 'gallerycarousel',
    images: me.images
  });

  // On clicking close icon, hide the carousel 
  // and destroy it after a certain perdiod
  galleryCarousel.element.on('tap', function (e, el) {
    galleryCarousel.hide(true);

    Ext.defer(function () {
      Ext.Viewport.remove(galleryCarousel);
    }, 300);
  }, this, {
    delegate: 'div[data-action="close_carousel"]'
  });

  // Get the image index which is clicked
  while ((clickedImage = clickedImage.previousSibling) != null) {
    clickedImgIndex++;
  }

  // Set the clicked image container as the active item of the carousel
  galleryCarousel.setActiveItem(clickedImgIndex);

  // Show the carousel
  galleryCarousel.show();
}

In this function, we first create an instance of the Gallery Carousel passing all the images as an array. In addition, we keep a track of the image index which is selected so that we can activate that Container in the Carousel. Last task is to close and destroy the carousel while the “close” icon is tapped. We add  a certain time delay before destroying the carousel in order to maintain the hide transition. And here is how the full screen image looks:

gallery_4

 

So, this is the first part of the Mosaic Gallery component. In next version, we probably will add pinch-to-zoom, image info and some more functionality.