Creating Mobile Apps with Appcelerator Titanium
上QQ阅读APP看书,第一时间看更新

Coding the application

Now that we have a better idea of how our application will look, and most importantly, how it will be structured, we will go ahead and translate that into code.

Let's do some scaffolding

We can start creating our window and views, and add controls to them. While these controls won't have any interaction (yet), it will be much easier down the road to implement interactions once everything is in place. As we did before, we will open the app.js file and clear all its content in order to have a clean slate.

Since every single application needs at least one window, ours is no exception to the rule. So, we will create one using the Ti.UI.createWindow function and set its title to Sili. We also need to add additional views and controls later on in the code, hence we store its reference in the win variable:

var win = Ti.UI.createWindow({
  backgroundColor: '#ffffff',
  title: 'Sili',
  layout: 'vertical'
});

Our top view will serve as a header containing the application's title as well as two buttons. We want it to have a dark blue background. It will fill the entire width of the screen and will span 10% of the screen's height:

var headerView = Ti.UI.createView({
  height: '10%',
  width: '100%',
  backgroundColor: '#002EB8'
});

We also need to create a label, whose text will be white and will be vertically aligned with a more prominent font than conventional labels. It will also use the window's title so that they both share exactly the same value:

headerView.add(Ti.UI.createLabel({ 
  text: win.title,
  left: 7,
  color: '#ffffff',
  height: Ti.UI.FILL,
  verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_CENTER,
  font:{
    fontSize: '22sp',
    fontWeight: 'bold'
  } 
}));
Tip

You may have noticed that the label's reference was not stored in a variable. This is a shortcut you can use when you need to add a control to a container and you know you won't be accessing it down the line. This makes for a linear code and removes the overhead of creating a new variable on the device during execution.

The header view also contains two buttons for interaction. One to activate the list's edit mode and another one to deactivate it. We'll get to that later, but for now, let's create and add them to the view.

The important thing to notice here is that both of the buttons will be located at exactly at the same location on the screen. This will give the effect of using a single button to toggle the list's edit mode. While in fact, the user will be interacting with two different buttons that are shown (or hidden), depending on the context. Therefore, one button will have the visible property set to false and the other one set to true:

var edit = Titanium.UI.createButton({
  title: 'Edit',
  right: 5,
  visible: true
});
var done = Titanium.UI.createButton({
  title: 'Done',
  right: 5,
  visible: false
});

We then add each button to the container view:

headerView.add(edit);
headerView.add(done);

Finally, we add the headerView object to the main window:

win.add(headerView);

The second container view will contain a list of all the audio recordings saved by the user. It will occupy 65% of the screen's height and 100% width:

var recordingsView = Ti.UI.createView({
  height: '65%',
  width: '100%'
});

The TableView component is best suited for displaying items in the form of lists. Since it will be contained in the view created previously (recordingsView), we simply use the Ti.UI.FILL constant for setting the TableView component's width and height properties. With this, the component will grow accordingly in order to occupy its parent's entire surface. We also make it editable in order to allow the users to modify the table's content:

var table = Ti.UI.createTableView({
  width: Ti.UI.FILL,
  height: Ti.UI.FILL,
  editable: true
});

We will then add the table to its parent view and the newly created view to the main window using the following code:

recordingsView.add(table);
win.add(recordingsView);

Our last view will be placed at the bottom of the screen and will contain a single, large button. It will have a dark background, and will span 25% of the screen's height, and 100% of the width:

var buttonView = Ti.UI.createView({
  width: '100%',
  height: '25%',
  backgroundColor: '#404040'
});

Now, if you remember the user interface structure schematics, you may have noticed a round button. Since such a look and feel is not achievable using regular buttons, we shall rely on an ImageView object and use it just as we would use a regular button. We simply assign it an image and set its height property to 95% to provide some padding, as shown in the following code:

var recordButton = Ti.UI.createImageView({
  image: '/images/recording_off.png',
  height: '95%'
});
Tip

We haven't set any height to our ImageView component, since the image file has the same width and height (a perfect square). Therefore, the ImageView component will automatically scale while keeping the image proportions intact.

As before, we will add the image view to its parent view and then add the said parent view to the main window, using the following code:

buttonView.add(recordButton);
win.add(buttonView);

In order to actually see something on the screen, we need to display the window:

win.open();
Let's see how this looks

Now that all user interface elements are in place, we are ready for our first run. For this, all we need to do is just click on the Run button from the App Explorer tab. It was mentioned at the beginning of this chapter that you need an actual device to run this application. At this stage, there is no specific code that would prevent you from running it using the simulator.

The following screenshot appears on our first run:

Of course, these controls don't respond to any user input and the list is empty. But this now gives us the structure in which we need to include the core logic of the application.

Recording with a click on a single button

Now let's dive into our application's main feature, the audio recording. Titanium provides us with an AudioRecorder object that can be used to record from the device's microphone.

It goes pretty much similar to the following steps:

  1. We create an AudioRecorder object.
  2. We start the recording using the start function.
  3. We stop the recording using the stop function.
  4. This function will return a File object, which we can interact with and save on the device's filesystem.

The first thing we need to do is to set the audio session mode. There are a few modes to choose from, depending on the type of recordings you want to do. In our specific case, we want to be able to record and playback, but not simultaneously. This can be done using the following code:

Ti.Media.audioSessionMode = Ti.Media.AUDIO_SESSION_MODE_PLAY_AND_RECORD;

Next, we need to create our AudioRecorder object and store its reference in a variable named recorder, in order to interact with this object in the code:

var recorder = Ti.Media.createAudioRecorder();

We also need to set the audio properties of our recorder. Again, there are many options and combinations to choose from. But in our case, we will use the 8-bit ULAW encoding format, which is quite appropriate for voice recording. As for the file format, we will use WAVE files. It could have been MP3 or another format, but WAVE files will be easier to use later on for playback:

recorder.compression = Ti.Media.AUDIO_FORMAT_ULAW;
recorder.format = Ti.Media.AUDIO_FILEFORMAT_WAVE;

Now that we have our audio recorder in place, let's add some behavior to our big Record button using the click event handler. Since we are using only one button to start and to stop recording, we need to have a different behavior depending on whether we are recording or not. To achieve this, we can rely on the recording property of the recorder. If we are recording, then we stop; if we are not, simply start recording. We also need to change the button's image in order to give a visual indication to the user that the application is actually recording:

recordButton.addEventListener('click', function(e) {
  if (recorder.recording) {
    var buffer = recorder.stop();
    e.source.image = '/images/recording_off.png';
  } else {
    recorder.start();
    e.source.image = '/images/recording_on.png';
  }
});
Tip

To change the button's image, we can just use the recordButton variable. But another way of doing it is through the source of the event. Most events pass a variable to the event function (e in our case). Logically, e.source should return the button itself. This is very useful while working dynamically with created controls, and we don't know which specific button has called the event handler. This is usually called the DRY (Don't repeat yourself) principle, meaning that there should only be one copy of any non-trivial piece of code. This is considered good practice.

What good is a buffer if we're not using it?

For now, when the recorder stops, the return value is stored in a variable named buffer. But since we don't do anything with it after that, the recording is lost. Let's remedy that by saving the buffer to the devices filesystem.

First, we must determine where the recordings will be stored. Titanium provides access to the application's data directory using a simple property (Ti.Filesystem.applicationDataDirectory). We generally use this directory to store application-specific files, which is the perfect location to store our recordings.

Let's create a variable that will act as a shortcut for the application data directory, thus making the code more concise:

var APP_DATA_DIR = Ti.Filesystem.applicationDataDirectory; 

We need to retrieve the recording's content right when the user stops recording and write it onto the filesystem. To do this, we create a new empty file using the getFile function with the location and the filename as parameters. In order for each file to have a unique name, we use the getTime function of the Date object (which returns the number of milliseconds since 1970/01/01) and append the .wav file extension.

Once the new file is created, we write the entire content of the buffer onto it using the following code:

recordButton.addEventListener('click', function(e) {
  if (recorder.recording) {
    var buffer = recorder.stop();
    var newFile =Titanium.Filesystem.getFile(APP_DATA_DIR,
      new Date().getTime() + '.wav');

    newFile.remoteBackup = true;
    newFile.write(buffer);

    e.source.image = '/images/recording_off.png';
  } else { 
// else code doesn't change
Note

Setting the remote backup to true will ensure that the file will be backed up to Apple's Cloud service, meaning that in cases where the user reinitializes his or her device, the recording he or she created will not be lost.

Listing stored recordings

Even though we have the ability to record multiple recordings and save them to the device, there is no way to know for sure that the recordings are indeed on the device. We need to create a function that will read all of the files contained in the application directory, loop through each of them, and then fill the list view.

In the declaration section, we get the reference application data directory and we list all of the files contained in the directory. Since the table view is expecting an array for its content, we create an empty array that will be filled later on in the function:

function loadExistingAudioFiles() {
  var dir = Ti.Filesystem.getFile(APP_DATA_DIR);
  var files = dir.getDirectoryListing();
  var tableData = []

We now need to loop through the list of files found in the directory using the following code:

for (var i = 0; i <files.length; i++) {

But since the list only contains the filenames and not the objects themselves, we must first instantiate each file in order to retrieve the metadata, such as the timestamp of the creation, since the only way that we can distinguish one recording from another is by its timestamp. We will then format this information and use it as the label for each row:

var recording = Ti.Filesystem.getFile(APP_DATA_DIR, files[i]);
var ts = new Date(recording.createTimestamp());
var rowLabel = String.formatDate(ts, 'medium') + ' - '+ String.formatTime(ts);

We can now create the TableViewRow object using the value we formatted previously as its title attribute:

var row = Ti.UI.createTableViewRow({ 
  title: rowLabel,
  leftImage: '/images/tape.png',
  color: '#404040',
  className: 'recording',
  font:{
    fontSize: '24sp',
    fontWeight: 'bold'
  },
 fileName: APP_DATA_DIR + '/' + recording.name
});
Note

Pay close attention to the fileName attribute. This property is not a part of the Titanium API, but since we are using JavaScript, we have the liberty to add additional properties which we can use later on in our code. This is very useful in a case similar to ours, where we will need extra information when the user selects a row from the list.

Once the row is created, we add it to the data array and once we are done creating all of the TableViewRow objects, we set the array to the table view using the following code:

tableData.push(row);
} // for end
table.setData(tableData);
} // function end
Be sure to call it!

Even if we had already made several recordings, the current code base won't show anything in the list. The reason is quite simple; the following function is called from anywhere in the code:

loadExistingAudioFiles();

To the following locations:

  • Right before the main window opens (this will load the list when the application launches)
  • Inside the recordButton click event handler, right after the file is written on the device (this will refresh the list every time a new recording is created)

Now that our function is called, we now have a list of all our recordings on the screen, as shown in the following figure:

Listening to a recording

The playback functionality of our application is pretty straightforward. When the user selects an item from the list, it will be played. This is where Titanium shines with simplicity, thanks to the Titanium.Media.Sound object that allows audio playback with very little code.

First, we need to add an event listener for the click event on the table view. This will give us access to the selected row through the e.rowData variable. Since this is the same as the table view row we created earlier, it has the fileName custom property that we assigned to it. It represents the file's complete path and will be used as the url property to create the sound object. We then start the playback using the following code to have a pretty functional voice recorder:

table.addEventListener('click', function(e) {
  var sound = Ti.Media.createSound({ 
    url: e.rowData.fileName
  });
  sound.play();
});

Deleting old recordings

The last feature missing from our application is the ability to delete recordings. To achieve this, we will be using the two buttons from the header view (one visible and one invisible at any given time) as well as the table view's built-in editing functionality.

The user interactions will be as follows:

  1. When the user switches onto the edit mode by clicking on the Edit button, the following events will happen:
    • The table view's editing mode is switched on
    • The Edit button turns invisible
    • The Done button turns visible
  2. From there, the user can select rows for deletion and confirm by clicking on the Delete button generated by the table view.
  3. When the user comes out of the edit mode by clicking on the Done button, the following events will happen:
    • The table view's editing mode is switched off
    • The Edit button turns back visible
    • The Done button turns invisible

Can you see a pattern here? No matter which button the user clicks on, it does the complete opposite of each operation. In the cases similar to this, it is easier to encapsulate these changes into a single function that sets each value to its opposite using the following code:

function toggleEditMode() {
  edit.visible= !edit.visible;
  done.visible= !done.visible;
  table.editing= !table.editing;
}

Once the logic is coded in a single function, both buttons call it and will have the desired effect:

edit.addEventListener('click', function(e) {
  toggleEditMode();
});
done.addEventListener('click', function() {
  toggleEditMode();
});

If we now run the application, we will observe the desired effect. The table view's edit mode allows us to delete rows from the list, and the two buttons will act as a toggle between the two modes, as shown in the following screenshot:

Now, if you look a little more closely, you will notice that we are not fully there yet. While the rows are deleted from the list, it is not (yet) the case for the actual recordings. In fact, if you were to delete a few rows, shut down the application, and restart it, you will notice that all the recordings are back.

In order for changes on the table to be reflected on the filesystem, we need to add an event listener to the table view for the delete event. Inside this listener, we will retrieve the complete file path from the row and instantiate a file object matching the said path. We will then delete the file using the following code:

table.addEventListener('delete', function(e) {
  var fileToDelete = Ti.Filesystem.getFile(e.rowData.fileName);
  
  fileToDelete.deleteFile();
});
Tip

You don't need to delete the row from the table view since this is done automatically by the component. The event listener serves as an "extra action" that you would want to do when the visual row is deleted.

This is it for our second mobile application. A complete audio-recorder that allows you to record audio, save them on your device, play them back, and delete them when needed. You can, of course, imagine a lot of new features to add based on the code you already have, such as volume control, audio quality, files format, and many others. Feel free to experiment with what the framework can offer and see the endless possibilities.