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:
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
andtouchend
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:
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.