Speed Reader in Ionic

Building a Custom Speed Reading Component in Ionic



·

Recently, I released a two part screencast where I walked through building a speed reading component in Ionic. These type of videos typically revolve around me attempting to create something from scratch without anything planned out beforehand, so often the final product is close but not completely finished. After I record the video, I generally spend some time coming up with a more polished version.

In this tutorial, we will walk through building the finalised version of the speed reading component in Ionic. Here’s what it will look like:

Ionic Speed Reader

The basic idea is that you can supply the component with an array of words, and it will cycles through those words and display them on screen. The user will be able to hold down to cycle through the words, let go to stop, pan up to increase the word per minute, pan down to decrease the words per minute, and go backward through the words by holding on the left side of the screen.

Even if you are not interested in building a speed reading component specifically, there may be some useful information in this tutorial for you. Concepts we will be covering include:

  • Using touchstart and touchend events to trigger functions
  • Using Hammer.js (the gesture library that Ionic uses behind the scenes) to utilise pan events
  • Using intervals to perform repetitive operations
  • Using a flex layout to center content both horizontally and vertically on the screen

Before We Get Started

Last updated for Ionic 3.0.1

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

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

1. Generate a New Ionic Application

We will start by generating a new Ionic application with the following command:

ionic start readspeeder blank --v2

Once that has finished generating you should make it your working directory:

cd readspeeder

We will need to create a provider that will supply us with the data for the speed reading component to use:

ionic g provider TextData

and we are also going to generate the custom component for the speed reader now, so you should run the following command:

ionic g component SpeedReader

NOTE: We will not be using lazy loading in this tutorial, so you should delete the auto-generated speed-reader.module.ts file.

In order to be able to use the speed reading component and the text data provider throughout the application, we will need to set them up in the app.module.ts file.

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

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { TextData } from '../providers/text-data';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { SpeedReader } from '../components/speed-reader/speed-reader';

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

2. Implement the Text Data Service

The first thing we are going to do is implement the TextData service. This isn’t particularly interesting, it will just allow us to grab the data for the speed reading component to use, and it’s just going to be some hard coded text for now. You may want to change this provider to pull the data in from some external source, or perhaps allow the user to supply it themselves.

Modify src/providers/text-data.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class TextData {

    speedReadingText: string = "The text that you want to speed read goes here";

    constructor(public http: Http) {
        // You may wish to make use of the Http service to load in the data instead
    }

    getSpeedReadingText(){
        return this.speedReadingText.split(" ");
    }

}

All we are doing here is manually defining a speedReadingText member variable that we can then access through the getSpeedReadingText function. This function does not just return it directly, though, instead it uses the split function to break the string up into an array of individual words (the string is “split” everywhere there is a space – you can also split on other characters like commas if you like).

3. Implement the Speed Reader Component

We are going to focus on implementing the logic for the speed reading component now. As I mentioned before, basically we want a space that will just display the words in the array one at a time, and in that space, we want to enable some controls which include:

  • Holding to cycle through the words, letting go to stop
  • Panning to increase of decrease the words per minute
  • Holding on the left side of the screen will cycle through the words backward

We’ll get the simple stuff out of the way first, let create the template first.

Modify src/components/speed-reader/speed-reader.html to reflect the following:

{{word}}

This is about as simple as a template will get, the template will just display whatever the value of this.word is in the TypeScript file.

Modify src/components/speed-reader/speed-reader.scss to reflect the following:

.ios, .md {

    speed-reader {

        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 2.5em;
        width: 100%;
        height: 100%;
        color: #fff;

    }

}

This just sets up some simple styles and a flexbox layout. We set the <speed-reader> component to take up 100% width and height, and we give it a flex layout so that we can make use of the justify-content and align-items properties. These properties will make sure that any children of this element will display in the center both vertically and horizontally.

Now let’s get into the meat of the component, this file is going to be a little trickier.

Modify src/components/speed-reader/speed-reader.ts to reflect the following:

import { Component, Input, ElementRef } from '@angular/core';

@Component({
    selector: 'speed-reader',
    templateUrl: 'speed-reader.html',
    host: {
        '(touchstart)': 'handleTouchStart($event)',
        '(touchend)': 'stopReading()'
    }
})
export class SpeedReader {

    @Input('textToRead') text;

    word: string = "";
    index: number = 0;
    textInterval: any;
    textSpeed: number = 200;
    direction: string = 'forward';
    playing: boolean = false;

    constructor(public element: ElementRef) {
        // FOR DEVELOPMENT ONLY
        //window.addEventListener("contextmenu", function(e) { e.preventDefault(); })
    }

    ngAfterViewInit() {

        let hammer = new window['Hammer'](this.element.nativeElement);
        hammer.get('pan').set({ direction: window['Hammer'].DIRECTION_ALL });

        hammer.on('pan', (ev) => {
          this.handlePan(ev);
        });

    }

    handleTouchStart(ev){

        clearInterval(this.textInterval);

        if(ev.touches[0].pageX < 100){
            this.direction = 'backward';
        } else {
            this.direction = 'forward';
        }

        this.startReading();
    }

    restartReading(){

        if(this.playing){
            clearInterval(this.textInterval);
            this.startReading();
        }

    }

    startReading(){

        this.playing = true;

        this.textInterval = setInterval(() => {

            this.word = this.text[this.index];

            if(this.index < this.text.length - 1 && this.direction == 'forward'){
                this.index++;
            }

            if(this.index >= 0 && this.direction == 'backward'){
                this.index--;
            } 

            if(this.index == -1 || this.index == this.text.length) {
                clearInterval(this.textInterval);
            }

        }, this.textSpeed);


    }

    stopReading(){

        this.playing = false;
        clearInterval(this.textInterval);

    }

    handlePan(ev){

        if(ev.additionalEvent === 'pandown'){
            this.textSpeed++;
            this.restartReading();
        } else if(ev.additionalEvent === 'panup') {
            this.textSpeed--;
            this.restartReading();
        } 

    }

}

Let’s step through what’s happening here. First, we set up a couple of event listeners on the host property:

    host: {
        '(touchstart)': 'handleTouchStart($event)',
        '(touchend)': 'stopReading()'
    }

When a user starts touching the screen we will trigger the handleTouchStart function, and when the user stops touching the screen we will trigger the stopReading function. We will discuss what those do in a moment.

We also set up some member variables and an @Input:

    @Input('textToRead') text;

    word: string = ""; // stores the current word to be displayed
    index: number = 0; // keeps track of where in array we are up to
    textInterval: any; // a reference to the interval which cycles through the words
    textSpeed: number = 200; // the speed at which the words cycle
    direction: string = 'forward'; // whether the words should cycle forwards or backwards
    playing: boolean = false; // whether the words are currently cycling or not

The input will allow us to grab the words that we want to display in this component, which we will eventually pass in like this:

<speed-reader [textToRead]="someArray"></speed-reader>

In the constructor we have a bit of code to help us debug through the browser:

    constructor(public element: ElementRef) {
        // FOR DEVELOPMENT ONLY
        //window.addEventListener("contextmenu", function(e) { e.preventDefault(); })
    }

When emulating a mobile device in the Chrome debugger, the context menu is triggered by holding down. This makes it very annoying to test our component because we need to perform the same gesture to trigger the speed reader, so if you uncomment this code it will disable that behaviour.

In the ngAfterViewInit function, we grab a reference to the Hammer.js object which Ionic uses behind the scenes for gesture recognition. By default, vertical panning is not enabled so we set up our own pan listener and enable it for all directions. This will then call the handlePan event whenever a pan event is detected.

The handleTouchStart function handles triggering the startReading function with the appropriate direction, it will also clear any currently active interval. If this function detects that the touch is within 100px of the left side of the screen it will set the direction to ‘backwards’.

The restartReading function is used to reset the speed that the words play at. The handlePan function that we implement a little later changes the textSpeed so in order for that to take effect we need to clear the current interval and start a new one.

The startReading function creates a new interval using textSpeed as the interval. This means that if the textSpeed were 1000 then the code inside the interval would run once every one second (because 1000 means 1000ms which is 1 second). The code inside of the interval just handles changing the current word according to the index and then either increases of decreases the index according to the direction. If either end of the array is reached, then the interval is cleared.

The stopReading function simply clears the interval, which will stop the words from cycling. The handlePan event, which is triggered whenever a pan event occurs, will detect the direction of the pan and then either increase of decrease the textSpeed. It will then call the restartReading function so that the new speed will take effect.

4. Use the Speed Reader Component

The speed reader component is finished at this point, now we just need to make use of it. We will just be adding it to your home page and adding a few styles to make it look nice. To start off with, we are going to change the background colour of the application.

Add the following Shared Variable to src/theme/variables.scss

$background-color: #1abc9c;

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

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TextData } from '../../providers/text-data';

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

    text: any;

    constructor(public navCtrl: NavController, public textService: TextData) {

    }

    ionViewDidLoad(){
        this.text = this.textService.getSpeedReadingText();
    }

}

All we are doing here is setting up the TextData service so that we can pull that speed reading data in, and then we set it on a text member variable so that we can access it in the template.

Now we just need to modify the template to make use of the speed reader component.

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

<ion-content no-bounce>

    <speed-reader [textToRead]="text"></speed-reader>

</ion-content>

We pass in the text variable as the input for textToRead, and it’s also important to add the no-bounce attribute to <ion-content>. Without that, the component is very awkward to use on a real device because the view will scroll as you are attempting to pan up and down.

You should now have something that looks like this:

Ionic Speed Reader

Summary

There’s certainly more that could be done here to improve usability if you wanted people to actually use this, but we have the basic functionally for the speed reader component complete. In terms of the controls, the way it is set up is quite easy to use, but you would likely want to add some additional elements to the application to better indicate to the user how to control the component.

What to watch next...