Samples

Very specific examples of implementation through code on various projects.

Examining the Flojuggler Home Page Animation JS


Flojuggler

I spent a fair amount of effort getting the animation on the Flojuggler home page just right. I felt like too many sites were doing horizontal slide shows on their home pages and I wanted this one to stand out.

There were no CSS animations/transitions when I wrote the code so it relies completely on JQuery methods. It loads the initial images in the HTML, checks to see if the browser can run the animation, then loops the two animation methods if it can. If the client cannot run the animation you see the static images displayed above so it still has impact.

I broke the animation into two methods so I could focus on the timing of each individually.

function animPeople() {
	if (navigator.appName == "Microsoft Internet Explorer") {
	  $(".iphone").slideUp();
	  $(".demo").slideUp(function(){
		  $(".hipster").slideDown(250);
		  $(".oh").slideDown(500);
		  $(".girl").slideDown(1000, function(){
			  $('body').wait(timer, function(){
				animDemo();
			  });
		  });
	  });
	} else {
	  $(".iphone").fadeOut();
	  $(".demo").fadeOut(function(){
		  $(".hipster").slideDown(250);
		  $(".oh").slideDown(500);
		  $(".girl").slideDown(1000, function(){
			  $('body').wait(timer, function(){
				animDemo();
			  });
		  });
	  });
	}
}

function animDemo() {
	if (navigator.appName == "Microsoft Internet Explorer") {
		$(".girl").slideUp();
		$(".oh").slideUp();
		$(".hipster").slideUp(function(){
			$(".demo").slideDown();
			$(".iphone").wait(timer/2).slideDown(function(){
				$('body').wait(timer, function(){
				  animPeople();
				});
			});
		});
	} else {
		$(".girl").fadeOut();
		$(".oh").fadeOut();
		$(".hipster").fadeOut(function(){
			$(".demo").slideDown();
			$(".iphone").wait(timer/2).show('bounce', function(){
				$('body').wait(timer, function(){
				  animPeople();
				});
			});
		});
	}

}

$(window).load(function () {
	timer = 6000;
	$(".hipster").slideDown(500);
	$(".girl").slideDown(1500);
	$(".oh").slideDown(1000,function(){
	  $('body').wait(timer, function(){
		  animDemo();
		});
	});
});

Saving and Deleting images via PhoneGap’s File API

The PhoneGap File API is a bit confusing to me and quite a few other people. I spent a fair amount of time looking for some “plain English” explanations and examples of storing images  but most tutorials and blog posts covered text files only. That’s great when you need to append or edit a data file but I needed to save images captured on an iPhone. It took a day of wrangling but I finally got my head wrapped around the basic API. Spent the next day building a set of methods. Let’s take a look.

I’m using PhoneGap (Cordova) 3.3 for this demo. It’s the latest stable build at the time of this writing. Seeing as PhoneGap will always be playing catch up with any changes to iOS it’s pretty important to check the docs and see if there’s been any major changes.

The Objective
We want to hit a button in the app and access the iOS photo gallery. From there we can navigate the gallery, edit a selected image, and then associate that image with a record. Pretty standard stuff for apps like Contacts, etc. This use case will need to use two of of the main API’s, Camera and File.

The Restrictions
When you call the Apple Photos App from your application the selected image file is stored in the host application’s tmp directory. This gives you a path to the file that looks something like this when you run the app in the simulator.

file:///Users/[name]/Library/Application%20Support/iPhone%20Simulator/7.0.3-64/Applications/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/tmp/cdv_photo_001.jpg.

Note the “tmp” part. That means that the file is now stored in a directory that is erased fairly often by iOS or by direct user action. If you have a reference to that file in a src attribute and the directory has been emptied to clear memory then the image breaks because it is nonexistent. When the user chooses an image we need to move (not copy) it to a persistent directory, grab the new URI, then store that new URI in the database. I say not copy because we’re looking to save drive space so it’s nice to move the file to a new location instead of creating a double.

The Code
The Phonegap best practice is to fire off your own code within the DOM ready event so we’ll start with that. When the event is fired it will log the event then run the requestFileSystem() method on line 13. The details are covered through comments.

// deviceready Event Handler
    //
    // The scope of 'this' is the event. In order to call the 'receivedEvent'
    // function, we must explicity call 'app.receivedEvent(...);'
    onDeviceReady: function() {
        app.receivedEvent('deviceready');
    },
    // Update DOM on a Received Event
    receivedEvent: function(id) {
        console.log('Received Event: ' + id);
        // request a new file system object then pass that to the gotFS() method.
        // gotFS() is a simple setter for a global var that the FileIO methods will use later.
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, FileIO.gotFS, FileIO.errorHandler);
    }

The second code snippet is directed to the Camera API and has three simple methods. Line 1 assigns a JQuery event handler to our button. In this case it’s been given a class of “.thumbnail-button”. When the DOM element is clicked it will execute the getPhoto() method defined on line 5. Passing the (pictureSource) parameter into the method makes the call more flexible. We’re using (pictureSource.PHOTOLIBRARY) to go directly to the iOS photo gallery and choose a photo. Had the objective been to take a new photo then we would pass (pictureSource.CAMERA) which opens the Apple Camera App instead of the Apple Photos App.

The getPhoto() method has a single call inside of it, navigator.camera.getPicture(), which takes three arguments; success callback, failure callback, and optional parameters.

I won’t get into the optional parameter set of the navigator.camera.getPicture() method because they should be fairly self explanatory. I would like to call attention to the allowEdit : true on line 11. This makes sure that we get a an image cropped to a perfect square by forcing the user into the edit mode after they tap on an image within the photo gallery and gives the user a chance to really zoom in on an area. Since we’re using these images for thumbnails we want square images with clear faces.

If the image is successfully saved the URI is passed to the onPhotoURISuccess() method on line 15 which passes that URI over to the FileIO portion of the task. We’re halfway done!


$(".thumbnail-button").on('click', function() {
getPhoto(pictureSource.PHOTOLIBRARY);
});

function getPhoto(source) {
    // Retrieve image file location from specified source
    navigator.camera.getPicture(onPhotoURISuccess, onFail, { quality: 50,
                                destinationType: destinationType.FILE_URI,
                                saveToPhotoAlbum: false,
                                sourceType: source,
                                allowEdit: true });
}

// Called when a photo is successfully retrieved
function onPhotoURISuccess(imageURI) {
    FileIO.updateCameraImages(imageURI);
}

// Called if something bad happens.
function onFail(message) {
    console.log('Failed because: ' + message);
}

Now we’ve got a new image sitting in the app’s tmp directory waiting for us to move it to a persistent location. We’ve taken the file URI of that image file and passed it over to another set of methods in the FIleIO name space. I’ve added comments to run through the sequence.

// set some globals
var gImageURI = '';
var gFileSystem = {};

var FileIO = {

// sets the filesystem to the global var gFileSystem
 gotFS : function(fileSystem) {
      gFileSystem = fileSystem;
 },

// pickup the URI from the Camera edit and assign it to the global var gImageURI
// create a filesystem object called a 'file entry' based on the image URI
// pass that file entry over to gotImageURI()
updateCameraImages : function(imageURI) {
        gImageURI = imageURI;
        window.resolveLocalFileSystemURI(imageURI, FileIO.gotImageURI, FileIO.errorHandler);
    },

// pickup the file entry, rename it, and move the file to the app's root directory.
// on success run the movedImageSuccess() method
 gotImageURI : function(fileEntry) {
       var newName = "thumbnail_" + gCurrentFlo + ".jpg";
       fileEntry.moveTo(gFileSystem.root, newName, FileIO.movedImageSuccess, FileIO.errorHandler);
 },

// send the full URI of the moved image to the updateImageSrc() method which does some DOM manipulation
 movedImageSuccess : function(fileEntry) {
      updateImageSrc(fileEntry.fullPath);
 },

// get a new file entry for the moved image when the user hits the delete button
// pass the file entry to removeFile()
 removeDeletedImage : function(imageURI){
      window.resolveLocalFileSystemURI(imageURI, FileIO.removeFile, FileIO.errorHandler);
 },

// delete the file
 removeFile : function(fileEntry){
      fileEntry.remove();
 },

// simple error handler
 errorHandler : function(e) {
       var msg = '';
       switch (e.code) {
       case FileError.QUOTA_EXCEEDED_ERR:
               msg = 'QUOTA_EXCEEDED_ERR';
               break;
        case FileError.NOT_FOUND_ERR:
               msg = 'NOT_FOUND_ERR';
               break;
        case FileError.SECURITY_ERR:
               msg = 'SECURITY_ERR';
               break;
        case FileError.INVALID_MODIFICATION_ERR:
               msg = 'INVALID_MODIFICATION_ERR';
               break;
        case FileError.INVALID_STATE_ERR:
               msg = 'INVALID_STATE_ERR';
               break;
        default:
               msg = e.code;
        break;
 };
       console.log('Error: ' + msg);
 }
}

IOS local notifications with Cordova v3+

Andrew Dahlman’s local notifications plug in is a life saver. He’s bridged the gap between the native Objective-C library and your standard JavaScript app sitting in the web view wrapper. I know that he submitted the plug in to the official Phonegap build but I’m not sure if it made it in. It should!

Like many open source projects, the original writer can’t stay on the project forever. When updates to dependencies occur software can become difficult to install/use. Such is the case with this amazingly useful code collection.

So I’m writing this post in order to document some of the issues I ran into along with some solutions. Feel free to comment, correct, or add to the show.

*Note: These are all Objective-C or Xml changes. The Javascript source gave me no issues at all.

1. The plist paradigm has been replaced with config.xml. The read me files refer to a plist file a few times and this can throw some folks off. There was at least one person on the GIT thread that mentioned this.

With the switch to config.xml comes the XML syntax.

<feature name="LocalNotification">
 <param name="ios-package" value="LocalNotification" />
 </feature>

2. The Cordova 2.3.0+ implementation is was missing the badge feature that Andrew had made available in the original Phonegap version. If you find local notifications useful for your app then putting badge numbers on the app icon seems pretty clutch so I added it. Line 16 inserts the badge option while lines 18-19 are for the background and foreground callback features that I personally did not need and left them commented. 


-(void)addNotification:(CDVInvokedUrlCommand*)command {

 NSMutableDictionary *repeatDict = [[NSMutableDictionary alloc] init];
 [repeatDict setObject:[NSNumber numberWithInt:NSDayCalendarUnit ] forKey:@"daily" ];
 [repeatDict setObject:[NSNumber numberWithInt:NSWeekCalendarUnit ] forKey:@"weekly" ];
 [repeatDict setObject:[NSNumber numberWithInt:NSMonthCalendarUnit ] forKey:@"monthly" ];
 [repeatDict setObject:[NSNumber numberWithInt:NSYearCalendarUnit ] forKey:@"yearly" ];
 [repeatDict setObject:[NSNumber numberWithInt:0 ] forKey:@"" ];

 UILocalNotification* notif = [[UILocalNotification alloc] init];

double fireDate = [[command.arguments objectAtIndex:0] doubleValue];
 NSString *alertBody = [command.arguments objectAtIndex:1];
 NSNumber *repeatInterval = [command.arguments objectAtIndex:2];
 NSString *soundName = [command.arguments objectAtIndex:3];
 NSNumber *badge = [command.arguments objectAtIndex:4];
 NSString *notificationId = [command.arguments objectAtIndex:5];
// NSString *bg = [command.arguments objectAtIndex:6];
// NSString *fg = [command.arguments objectAtIndex:7];

 notif.alertBody = ([alertBody isEqualToString:@""])?nil:alertBody;
 notif.fireDate = [NSDate dateWithTimeIntervalSince1970:fireDate];
 notif.repeatInterval = [[repeatDict objectForKey: repeatInterval] intValue];
 notif.soundName = soundName;
 notif.timeZone = [NSTimeZone defaultTimeZone];

 NSDictionary *userDict = [NSDictionary dictionaryWithObjectsAndKeys:
 notificationId , @"notificationId",
 command.callbackId, @"callbackId",
 nil
 ];

 notif.userInfo = userDict;

 [[UIApplication sharedApplication] scheduleLocalNotification:notif];

}

3. For some reason the call to the application object in the actual badge call was not working so I had to call it using the sharedApplication syntax instead.

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
 {

 UIApplicationState state = [application applicationState];
 if (state == UIApplicationStateActive) {
 // WAS RUNNING
 NSLog(@"I was currently active Selino");

 NSString *notCB = [notification.userInfo objectForKey:@"foreground"];
 NSString *notID = [notification.userInfo objectForKey:@"notificationId"];

 NSString * jsCallBack = [NSString
 stringWithFormat:@"%@(%@)", notCB,notID];

 [self.viewController.webView stringByEvaluatingJavaScriptFromString:jsCallBack];

 //application.applicationIconBadgeNumber = 0;
 [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
 }
 else {
 // WAS IN BG
 NSLog(@"I was in the background");

 NSString *notCB = [notification.userInfo objectForKey:@"background"];
 NSString *notID = [notification.userInfo objectForKey:@"notificationId"];

 NSString * jsCallBack = [NSString
 stringWithFormat:@"%@(%@)", notCB,notID];
 [self.viewController.webView stringByEvaluatingJavaScriptFromString:jsCallBack];

 //application.applicationIconBadgeNumber = 0;
 [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
 }
 }

4. Badges were removed from the Cordova plugin properties altogether. I mentioned that earlier. So I added a badge call into the cancel method and added an incrementBadge method.

- (void)cancelAllNotifications:(CDVInvokedUrlCommand*)command {
[[UIApplication sharedApplication] cancelAllLocalNotifications];</pre>
[UIApplication sharedApplication].applicationIconBadgeNumber = 0;
}

- (void)incrementBadge:(NSNotification *)notification {
int num = [UIApplication sharedApplication].applicationIconBadgeNumber;
[UIApplication sharedApplication].applicationIconBadgeNumber = num + 1;
}