Skip to content
This repository has been archived by the owner on Mar 3, 2023. It is now read-only.

Ionic Discover Users Screen

Miroslav Smukov edited this page Jan 23, 2017 · 10 revisions

Discover Users Screen

I envisioned my app as a social app that will connect nearby users that have similar interests and goals. To achieve this functionality I need to offer my users a way to discover such users in their vicinity, and send them a contact request so they can start communicating. For this I decided to implement a Discover Users screen that will show the list of nearby users. The user will be able to swipe through this list and send the contact requests to other users. Let's see how I implemented the UI for this screen.

Laying out the foundation

I won't go again through the steps of adding new pages to the app, and the process of laying out the foundation for our Discover Users screen. Instead, you can checkout this commit and see the code for yourself.

The sliding functionality

The key component in this screen will be ion-slides. The ion-slides component is a multi-section container. Each section can be swiped or dragged between. It contains any number of ion-slide components. We'll use this component to show the list of nearby users through which our user can swipe through. You can see below the code snippet from the discoverUsersPage.html:

Source Code

  <ion-slides #slider
    (ionDidChange)="onSlideChanged()"
    [options]="sliderOptions"
    >
    <ion-slide *ngFor="let usr of users" class="user-slide">
      <profile-header
        [fullName]="usr.getFullName()"
        [profileImage]="usr.profileImage"
        ></profile-header>

      <ion-list no-lines style="margin-top:10px">
        <ion-item>
          <p>{{usr.employment}}</p>
        </ion-item>
        <ion-item class="no-bottom-border">
          <p>{{usr.education}}</p>
        </ion-item>
        <ion-item>
          <p class='section-name'>Interests</p>
          <p>{{usr.interests}}</p>
        </ion-item>
        <ion-item>
          <p class='section-name'>Knowledgeable In</p>
          <p>{{usr.knowledgeable}}</p>
        </ion-item>
        <ion-item>
          <p class='section-name'>Goals</p>
          <p>{{usr.currentGoals}}</p>
        </ion-item>
      </ion-list>
    </ion-slide>
    <ion-slide style="padding-bottom:50px;">
      No More Users Nearby
    </ion-slide>
  </ion-slides>

Above, we are using the ngFor to loop through nearby users and create a ion-slide component for each one, displaying the user details. Pretty simple, and similar to the ion-list and ion-item components.

We also have an additional, final slide. This slide will always be in the ion-slides component, and it will always be the last slide, showing the "No More Users Nearby" message.

Slide alignment

You may notice the class user-slide that is added to the ion-slide components inside the ngFor. This class is used to prevent the automatic centering of the slide content to the middle of the screen. This is achieved through following code in our discoverUsersPage.scss:

Source Code

.user-slide .slide-zoom {
  height: 100%;
}

Adding the FABs

We need a way for users to accept or discard the users they see. For this, we are going to add two FABs (Floating Action Buttons). Here's the code:

Source Code

  <button #btnDismiss fab fab-bottom fab-right secondary style="margin-bottom:75px; z-index: 100;"
    (click)="onBtnDismissClicked()"
    @fabState="btnState">
    <ion-icon name="close"></ion-icon>
  </button>
  <button #btnAccept fab fab-bottom fab-right secondary style="z-index: 100;"
    (click)="onBtnAcceptClicked()"
    @fabState="btnState">
    <ion-icon name="checkmark"></ion-icon>
  </button>

There's a few interesting things in this code snippet. The first one is the margin-bottom:75px style that is applied to the btnDismiss. This is added so that this button would be above the btnAccept.

The second thing to note is also a style setting, it's the z-index: 100. We needed to add this value so that our FABs would be visible above the slides.

The last thing is the @fabState="btnState" attribute. This is a syntax for attaching the Angular 2 animations. Angular 2 animations are super easy to add and super cool at the same time. We are using it here to animate the show/hide transition for our FAB buttons, depending if the user is on the last slide or not. The syntax above basically says to watch the btnState property on the component, and trigger the fabState animation if it changes.

Angular 2 Animations

In order to use Angular 2 animations in the component, we need to do a few things. First, we need to import the required components in the discoverUsersPage.js. You can see the full source code here, but I'm going to go over the important bits for the animations:

import{
  Input,
  trigger,
  state,
  style,
  transition,
  animate} from '@angular/core';

Next, we need to define our animation, and its behavior. We are doing this inside the @Component decorator:

@Component({
  templateUrl: 'build/pages/discoverUsersPage/discoverUsersPage.html',
  animations: [
    trigger('fabState',[
      state('inactive', style({
        transform: 'scale(0)'
      })),
      state('active',   style({
        transform: 'scale(1)'
      })),
      transition('inactive <=> active', animate('150ms ease-out'))
    ])
  ]
})

In the animations array above we are defining triggers for the animations. The fabState is the name of the trigger, and, as you saw in the code snippet above, that's the name of the attribute that we are using to bind the animation to our FABs, we just prefix that name with @ (e.g. - @fabState).

The state part defines the multiple states that the animation is triggered for. For example, in our code we are attaching our animation like so: @fabState="btnState". That means that we are listening for the change on the btnState property that is defined in our component. If the btnState is equals to inactive, our first state will be triggered and with it the style change, which says: transform: 'scale(0)'. On the other hand, if the btnState==='inactive', a new style transformation will be applied: transform: 'scale(1)'.

The last, transition part, defines from which state the animation can transition to the next, and also the duration and the animation style of that transition.

Our above animation simply states that if the btnState is active, the scale of the button will be 1 (or 100%), and the moment that the btnState changes to inactive, the scale will gradually lower to 0 (or 0%), during the 150ms period, easing out at the end. The same principle is applied when transitioning from inactive => active.

Because both of our states are transitioning between each other, and have the same timing, we can use one transition definition with the <=> direction. However, if we wanted to make one transition longer than the other one, we could have also defined them like this:

transition('inactive => active', animate('150ms ease-out')),
transition('active => active', animate('300ms ease-in')),

There's lot more flexibility that you can add to Angular 2 animations. I suggest to read this article for additional insights.

The component's logic

The full code of the component can be seen here. Instead of pasting it fully, I'll just go over the important bits.

  onBtnDismissClicked(){
    let currentIndex = this.slider.getActiveIndex();
    //TODO: update db
    this._slideAndRemove(currentIndex)
  }

  onBtnAcceptClicked(){
    let currentIndex = this.slider.getActiveIndex();
    //TODO: update db
    this._slideAndRemove(currentIndex)
  }

These are the two FAB click event handlers. At the moment they are doing the same thing, but they'll change over time. Basically, they are obtaining the index of the current slide being shown in the slider, and are forwarding it to the _slideAndRemove(..) function.

  _slideAndRemove(currentIndex){
    //don't remove the last "No More Users Nearby" slide
    if(this.slider.length() === 1 || currentIndex + 1 === this.slider.length())
      return;

    this.itemToDelete = currentIndex;
    this.slider.slideTo(currentIndex+1, 500);
  }

The _slideAndRemove(..) function takes the slide to remove index, checks to make sure that's not the last slide (we are not removing the last slide with our "No More Users Nearby" message), and sets it up for removal by assigning it to itemToDelete property. Then, it is calling the slideTo function on the slider, which makes the slider go to the next slide (the 500 part is the transition length in milliseconds). Finally, this transition makes the slider emit an event onSlideChanged.

  onSlideChanged(){
    let pendingDeletePosition = this.itemToDelete;
    if(pendingDeletePosition !== -1){
      this.itemToDelete = -1;
      this.users.splice(pendingDeletePosition, 1);
      //slider's slider is a swiper
      this.slider.slider.removeSlide(pendingDeletePosition);
      this.slider.slider.update();
    }

    if(this.slider.length() === 1 || this.slider.getActiveIndex() + 1 === this.slider.length()){
      console.log('inactive');
      this.btnState= 'inactive';
    }else{
      console.log('active');
      this.btnState= 'active';
    }
  }

The above event handler is the key for successful removal of the users (slides) from our slider. Basically, we don't want to remove the slide before the transition animation has been completed, so that the user has the smoothest experience.

The first IF statement removes a user from the model, and also removes the actual slide from the slider. We are then calling the slider.update() method, which should be called any time we are adding or removing slides in the slider. The removal of slides won't be triggered during the normal user swipe, because the itemToDelete will be -1, which is the behavior that we want, so the users can get a second chance to review the users nearby.

The second IF statement changes the btnState based on the slide that is currently visible, and that change will trigger our fabState animation that we described before.

Conclusion

That's the gist of the "Discover Users" page implementation. For more details, and access to full source code, you can use the link to specific commits below, or the links I attached throughout the article.

I had less problems with removal of slides in this Ionic2 implementation, than with Android Native approach. I'd say that this was easier than Android Native implementation of the same screen.

References

Commits

Clone this wiki locally