Panorama Feed in Ionic 2

Create a News Feed with 360-Degree Photo Viewing in Ionic 2 & 3



·

If you’ve been on Facebook recently, you will probably have seen interactive photos uploaded by your friends that you can drag around to view more of the photo. This is done in one of two ways, either the user uploaded a panorama photo, or a “true” 360-degree photo using something like Photo Sphere.

Before we go any further, I should clarify that the app we are going to build will only allow you to view panoramas, not photo sphere photos. In a tutorial earlier this week, I covered how to create a photo tilt component in Ionic 2, and we are going to be using that same concept here to view panoramas in a news feed by tilting the device back and forth. Here’s what it will look like:

Panorama Feed in Ionic 2

We are going to be making some performance and visual improvements to the photo tilt component, and we are also going to be making use of another recent tutorial where we built a high-performance parallax header directive to make the app look extra cool.

Before We Get Started

Last updated for Ionic 3.1.1

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

1. Generate a New Ionic 2 Application

We will start off by generating a new Ionic 2 application by running the following command:

ionic start ionic2-panarama-feed blank

Once the project has finished generating, make it your working directory by running the following command:

cd ionic2-panarama-feed

We are going to be using a custom component and a directive in this application, and usually, we would generate those with the Ionic CLI now, but we have already built both of them in previous tutorials, so you will need to make sure you add both of them to the components folder in your project.

If you haven’t already built the component and the directive, you can follow along with the tutorials below:

or you can just grab the source code directly below:

Make sure you have the parallax-header and photo-tilt folders added to your src/components folder (you will need to create this folder)

We are also going to be using some more code from yet another tutorial:

This isn’t strictly required, but we will be using it to make the application look a little nicer. I will be adding the code directly to this tutorial, so you don’t need to worry about setting anything else up beforehand, just make sure you have those two components set up in your components folder.

In order to be able to use the PhotoTilt component and the ParallaxHeader directive, we will need to add them to the app.module.ts file.

Modify src/app/app.module.ts to reflect the following:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { PhotoTiltComponent } from '../components/photo-tilt/photo-tilt';
import { ParallaxHeader } from '../components/parallax-header/parallax-header';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    PhotoTiltComponent,
    ParallaxHeader
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler}
  ]
})
export class AppModule {}

2. Modify the PhotoTilt Component

Now that we have everything set up, we are going to make a couple of modifications. We are going to add an additional @Input to the PhotoTilt component that will allow us to reverse the direction of the tilt. So rather than the image shifting left when the user tilts the device to the left, the image will instead shift to the right (and vice versa).

We are also going to add a performance improvement. The initial design of the component was intended to have a single image on the screen at a time, but now we want to display multiple images in a scrolling feed. This means that if we add 20 of these components to the feed, all 20 of them are going to animate as the device is tilted. Although most modern devices are likely capable of doing this, it is an unnecessary performance hit because there is never going to be more than 1 or 2 components on the screen at the same time. So, we are going to add a bit of extra code to check if the component is currently visible on screen before rendering the effect.

Modify src/components/photo-tilt/photo-tilt.ts to reflect the following:

import { Component, Input, ViewChild, Renderer } from '@angular/core';
import { Platform, DomController } from 'ionic-angular'

@Component({
  selector: 'photo-tilt',
  templateUrl: 'photo-tilt.html',
  host: {
    '(window:deviceorientation)': 'onDeviceOrientation($event)',
    '(window:resize)': 'initTilt()'
  }
})
export class PhotoTiltComponent {

  @Input('tiltImage') tiltImage: any;
  @Input('tiltHeight') tiltHeight: any;
  @Input('tiltReverse') tiltReverse: any;

  @ViewChild('mask') mask: any;
  @ViewChild('image') image: any;

  averageGamma: any = [];
  maxTilt: number = 20;
  latestTilt: any = 0;
  centerOffset: any;
  resizedImageWidth: any;
  aspectRatio: any;
  delta: any;
  height: any;
  width: any;

  constructor(public platform: Platform, public domCtrl: DomController, public renderer: Renderer) {

  }

  initTilt(){

      this.height = this.tiltHeight || this.platform.height();
      this.width = this.platform.width();

      this.aspectRatio = this.image.nativeElement.width / this.image.nativeElement.height;
      this.renderTilt();

  }

  renderTilt(){

    this.image.nativeElement.height = this.height;

    this.resizedImageWidth = this.aspectRatio * this.image.nativeElement.height;
    this.renderer.setElementStyle(this.image.nativeElement, 'width', this.resizedImageWidth + 'px');

    this.delta = this.resizedImageWidth - this.width;
    this.centerOffset = this.delta / 2;

    this.updatePosition();

  }

  onDeviceOrientation(ev){

    if(this.averageGamma.length > 8){
      this.averageGamma.shift();
    }

    this.averageGamma.push(ev.gamma);

    this.latestTilt = this.averageGamma.reduce((previous, current) => {
      return previous + current;
    }) / this.averageGamma.length;

    let imageStartPosition = this.image.nativeElement.y,
        imageEndPosition = imageStartPosition + this.image.nativeElement.height,
        viewportHeight = this.platform.height();

    // Only update position if the component is currently on the screen

    if(imageEndPosition > 0 && imageStartPosition < viewportHeight){

      this.domCtrl.write(() => {
        this.updatePosition();
      });

    }

  }

  updatePosition(){

    let tilt = this.latestTilt;

    if(tilt > 0){
      tilt = Math.min(tilt, this.maxTilt);
    } else {
      tilt = Math.max(tilt, this.maxTilt * -1);
    }

    if (this.tiltReverse) {
      tilt = tilt * -1;
    }

    let pxToMove = (tilt * this.centerOffset) / this.maxTilt;

    this.updateTiltImage((this.centerOffset + pxToMove) * -1);

  }

  updateTiltImage(pxToMove){
    this.renderer.setElementStyle(this.image.nativeElement, 'transform', 'translate3d(' + pxToMove + 'px,0,0)');
  }

}

Most of this code is the same as before, so if you would like a more detailed explanation I recommend going back and reading the previous tutorial.

You will notice that we have added a tiltReverse input, along with a little bit of code in updatePosition that negates the tilt value accordingly. Then we have this if statement added to the onDeviceOrientation function:

    let imageStartPosition = this.image.nativeElement.y,
        imageEndPosition = imageStartPosition + this.image.nativeElement.height,
        viewportHeight = this.platform.height();

    // Only update position if the component is currently on the screen

    if(imageEndPosition > 0 && imageStartPosition < viewportHeight){

      this.domCtrl.write(() => {
        this.updatePosition();
      });

    }

This just checks if the images current y position falls within the range that is currently viewable.

3. Set up the News Feed Template

Now that we have our component sorted, we are going to set up the template for the news feed. This is going to be almost exactly the same as the template we used in this tutorial, except that we are going to replace the static image in the feed with our PhotoTilt component.

Modify src/pages/home/home.html to reflect the following:

<ion-header no-border>
  <ion-navbar color="secondary">
    <ion-buttons left>
        <button ion-button icon-only color="light"><ion-icon name="arrow-back"></ion-icon></button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content fullscreen parallax-header>

    <div class="header-image"></div>

    <div class="main-content">

        <ion-segment [(ngModel)]="category">
            <ion-segment-button value="gear">
                Gear
            </ion-segment-button>
            <ion-segment-button value="clothing">
                Clothing
            </ion-segment-button>
            <ion-segment-button value="nutrition">
                Nutrition
            </ion-segment-button>
        </ion-segment>

        <ion-card *ngFor="let card of cards">

          <ion-item>
            <ion-avatar item-left>
              <img src="https://avatars.io/facebook/joshua.morony" />
            </ion-avatar>
            <h2>Josh Morony</h2>
            <p>November 5, 1955</p>
          </ion-item>

          <photo-tilt reverseTilt="true" tiltHeight="200" tiltImage="assets/images/panorama.jpg"></photo-tilt>

          <ion-card-content>
            Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.
          </ion-card-content>

          <ion-row>
            <ion-col>
              <button ion-button icon-left clear small>
                <ion-icon name="thumbs-up"></ion-icon>
                <div>12 Likes</div>
              </button>
            </ion-col>
            <ion-col>
              <button ion-button icon-left clear small>
                <ion-icon name="text"></ion-icon>
                <div>4 Comments</div>
              </button>
            </ion-col>
            <ion-col center text-center>
              <ion-note>
                11h ago
              </ion-note>
            </ion-col>
          </ion-row>

        </ion-card>

    </div>

</ion-content>

As you can see, setting up the component in the feed is as easy as adding this line:

<photo-tilt reverseTilt="true" tiltHeight="200" tiltImage="assets/images/panorama.jpg"></photo-tilt>

This time we have set reverseTilt to true, and manually supplied a height of 200 so that the image doesn’t take up the entire viewport height (which it does by default). I am using a locally stored image as the tiltImage (a panorama I took from a recent concert my fiancee and I went to) so you will either need to supply your own, or you can copy it from the source code of this application.

To finish off the news feed layout, we will also need to set up a couple of things in the TypeScript file.

Modify src/pages/home/home.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

    cards: any;
    category: string = 'gear';

    constructor(public navCtrl: NavController) {

        this.cards = new Array(10);

    }

}

We have an *ngFor loop in the template, so we are just creating an empty array for it to iterate over.

4. Add some Styling

Finally, we just need to add a bit of styling to the application. Most of this is just to make it look nice, and we have already covered it in the previous tutorial, but there are a couple of important additions.

Modify src/pages/home/home.scss to reflect the following:

.ios, .md {

    page-home {

        ion-content {
            background-image: url(../assets/images/header-image-blurred.png);
            background-size: contain;
            background-repeat: no-repeat;
        }

        .header-image {
            background-image: url(../assets/images/header-image.png);
            height: 40vh;
            z-index: 1;
        }

        .main-content {
            max-width: 100vw;
            background-color: map-get($colors, dark);
            box-shadow: 0px -1px 13px 1px rgba(0,0,0,0.3);
        }

        .scroll-content {
            padding-top: 0 !important;
        }

        ion-header {
            backdrop-filter: blur(8px);
        }

        ion-navbar {
            opacity: 0.4;
            box-shadow: 0px 1px 13px 1px rgba(0,0,0,0.77);
        }

        ion-segment {
            padding: 0px 12px;
            opacity: 0.7;
            position: relative;
            top: -50px;
        }

        ion-segment-button {
            border-color: map-get($colors, primary) !important;
            color: #fff !important;
        }

        ion-card {
            z-index: 2;
            background-color: #fff;
            width: 100%;
            margin: 0;
            margin-bottom: 25px;
            position: relative;
            top: -30px;
        }

        ion-card:last-of-type {
            margin-bottom: 0 !important;
        }

        ion-item {
            background-color: #fff;
        }

    }

}

We have added a max-width of 100% of the viewport width to the content area so that the image the PhotoTilt component is using (which is a long panorama) doesn’t break the bounds of the viewport and extend the width of all of the content.

Modify the Shared Variables and Named Color Variables in theme/variables.scss to reflect the following:

// Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Sass variables found in Ionic's source scss files.
// To view all the possible Ionic variables, see:
// http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/

$background-color: #3c353c;

// Named Color Variables
// --------------------------------------------------
// Named colors makes it easy to reuse colors on various components.
// It's highly recommended to change the default colors
// to match your app's branding. Ionic uses a Sass map of
// colors so you can add, rename and remove colors as needed.
// The "primary" color is the only required color in the map.

$colors: (
  primary:    #bdc3c7,
  secondary:  #7f8c8d,
  danger:     #f53d3d,
  light:      #fff,
  dark:       #3c353c
);

Summary

You should now have a pretty fancy looking news feed, which includes a header image with a parallax effect applied, and several panoramas that you can view through tilting your device left and right.

These effects can be quite tricky to pull off smoothly, especially on low-end mobile devices. But since we are making use of Ionic 2 performance features that help write to the DOM in a more efficient manner, and we have also implemented our own performance hack by only adding the effect to on-screen elements, this application should perform very well.

What to watch next...

  • Kp Abhiram

    Hi Josh,

    I am expecting 2 tuts daily instead of 1.

    Its just an expectation.

    thanks

  • Jigar Shah

    Hi Josh,

    When I load the Panorama image, it takes the width of the device and gets contained in it. So when I do the image tilt effect the blank space gets loaded as the image only occupies the phone width. Am I doing something wrong?