Developing an Android App using PhoneGap

Since Android was introduced I’ve been excited about the platform because iOS was always so limited it what you could do, you know the whole “walled garden” thing. Tinkering, exploring, and creating have always been my thing. Also, the Windows Mobile phones of the time were large and chunky with poor resistive touch screens. So, naturally I always wanted to write an app for Android but it always seemed to involve so much time trying to learn a new language (well I already somewhat knew Java) and a new platform/environment/ecosystem. Just because I knew and had previously done Java I was in way a wiz. Also the context of Android presented it’s own challenging of understanding how the underlying OS worked. Fast forward 4 years or so and I’ve finally created my first Android app (which should be able to be easily ported to run on iOS except that I refuse to give Apple $99 to let me put it in the app store). Surprising however I used very little Java and a whole lot more JavaScript, CSS, and HTML5.

I used PhoneGap, which seems to be gaining a lot of traction. Before I go into that let me back up and explain how I got there.

It all really started because I wanted to learn HTML5. I had been wanting to learn it since it burst onto the scene but like writing an Android app I just never put my mind to it. I finally found something that caught my interest enough that I pursued it. I wanted to be able to draw, like a paint application. A little research quickly turned up a canvas. I had a working sample in minutes…for the browser. Within a decent amount of time I had adapted it to be my own design, adjusting the layout to be full screen and then testing it in a mobile browser. Except that it didn’t work. A little research turned up that mobile browsers required a different set of events. Changing the mousedown event to a form of a touch start event fixed the issue in the browser…for my Android Gingerbread device. As soon as someone with an ICS device tested it I was out of luck again. A little more research turned up a different set of touch events for newer browsers. Once implemented I wanted to make it a native app on the phone but I didn’t want to change much code. After a little research and testing out nomad (which didn’t quite work for me but I may have not set something up right because I tend to not fully read instructions when tinkering, surprise!), landed on PhoneGap using Eclipse on OSX. It worked and I had Drawsaur in the Google Play store in no time. Want to know how I did it? Thought you’d never ask!

First off the only HTML5 tag I used was the…yeah you guessed it (if you’ve read everything to here), the canvas tag:

Sorry! Your browser doesn't support Canvas.

The rest was JavaScript, aside from being good and trying to follow layout rules to use CSS. In my js I found out that I needed to be able to get the canvas and context like this:

var canvas = document.getElementById("canvas"),
ctxt = canvas.getContext("2d");

I did some layout and other code but the next thing I found was that I need those oh so elusive events:

var self = {
//bind click events
init: function() {
canvas.addEventListener('touchstart', self.preDraw, false);
canvas.addEventListener('touchmove', self.draw, false);
},

return self.init();

When calling these events there was certain way in which the touches were, let me say best registered:

preDraw: function(event) {
$.each(event.touches, function(i, touch) {
var x = this.pageX - offset.left,
y = this.pageY - offset.top,
id = touch.identifier;
});
//prevent scrolling
event.preventDefault();
},

I also had to read and learn a lot about how to put images on the canvas:

var trash = new Image(); 
trash.src = "trashcan.png";
trash.onload = function() { ctxt.drawImage(trash, trashX, trashY-adWidth); }

Which turned out to be very similar to how you used the canvas and drew on the canvas in general. Like drawing a rectangle:

ctxt.rect(drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);

Or a circle:

ctxt.arc(cursor[i].location, locY, cursor[i].size, 0*Math.PI, 2*Math.PI);

In the end, the latest version of Drawsaur’s code looks like this:

/*********************************************************
* Drawsaur
* Created 9/2/2012 by Carlo Wahlstedt
*
* This code uses GPL v3. Please following accordingly.
*
* This code is distributed in the hope that it will be useful,
* WITHOUT ANY WARRANTY or implied warranty.
*
**********************************************************/
//this is what starts the drawing
$(function(){
var drawing = new DrawingApp();
});
//here is where all of the work is done
var DrawingApp = function(options) {
// grab canvas element
var canvas = document.getElementById("canvas"),
ctxt = canvas.getContext("2d");
hasAds = document.getElementById("ads"),
divColors = document.getElementById("colors"),
canvasColor = document.getElementById("colorCanvas"),
ctxtColor = colorCanvas.getContext("2d");
curColor = "black",
curSize = 2,
lines = [,,],
offset = $(canvas).offset(),
colorsShowing = false,
selColor = curColor;
adWidth = 50;

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvasColor.width = canvas.width - 25;
canvasColor.height = canvas.height - 170;

//if the ads aren't there then don't fix the layout
if (hasAds == null) { adWidth=0; }

//setup the canvas context
ctxt.lineWidth = 5;
ctxt.lineCap = "round";
ctxt.pX = undefined;
ctxt.pY = undefined;

//setup the drawing area
var drawingAreaX = 0,
drawingAreaY = 0,
drawingAreaWidth = canvas.width - 20,
drawingAreaHeight = canvas.height - (60 + adWidth);

//list of pen sizes
var cursor = [];
cursor.push({ size:2, radius:2, location:canvas.width-30 });
cursor.push({ size:5, radius:5, location:canvas.width-55 });
cursor.push({ size:7, radius:10, location:canvas.width-80 });
cursor.push({ size:10, radius:20, location:canvas.width-105 });

//draw the trashcan
var trashX = cursor[cursor.length-1].location-40,
trashY = canvas.height-40;
var trash = new Image();
trash.src = "trashcan.png";
trash.onload = function() { ctxt.drawImage(trash, trashX, trashY-adWidth); }

var colorX = 5,
colorY = canvas.height-45;
var iconColors = new Image();
iconColors.src = "colors.png";
iconColors.onload = function() { ctxt.drawImage(iconColors, colorX, colorY-adWidth); }

//where the magic happens
var self = {
//bind click events
init: function() {
canvas.addEventListener('touchstart', self.preDraw, false);
canvas.addEventListener('touchmove', self.draw, false);
},
//draws the rectangle
drawRect: function() {
ctxt.beginPath();
ctxt.rect(drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);
ctxt.closePath();
ctxt.lineWidth = "1";
ctxt.strokeStyle = "black";
ctxt.fillStyle = "white";
ctxt.fill();
ctxt.strokeRect(drawingAreaX, drawingAreaY, drawingAreaWidth+7, drawingAreaHeight+7);
},
//updates the UI to tell the user which pen size is now selected
drawCursors: function() {
var locY = canvas.height - (35 + adWidth);
for (i = 0; i < cursor.length; i++) {
ctxt.lineWidth = 4;
ctxt.beginPath();
ctxt.arc(cursor[i].location, locY, cursor[i].size, 0*Math.PI, 2*Math.PI);
ctxt.closePath();
ctxt.strokeStyle = "grey"
ctxt.stroke();
if (curSize == cursor[i].radius) {
ctxt.fillStyle = 'black';
ctxt.fill();
} else {
ctxt.fillStyle = 'white';
ctxt.fill();
}
}
},
//updates the UI to show the color picker
showColors: function() {
divColors.style.visibility = "visible";
divColors.style.display = "inline-block";
canvas.style.visible = "hidden";
canvas.style.display = "none";
colorsShowing = true;
},
//updates the UI to hide the color picker
hideColors: function() {
divColors.style.visibility = "hidden";
divColors.style.display = "none";
canvas.style.visible = "visible";
canvas.style.display = "inline";
colorsShowing = false;
},
//this is the single press event
preDraw: function(event) {
$.each(event.touches, function(i, touch) {
var x = this.pageX - offset.left,
y = this.pageY - offset.top,
id = touch.identifier;

//if it's in the drawing area then record the draw
if (y < drawingAreaHeight && x < drawingAreaWidth) {
lines[id] = { x : x,
y : y,
color : curColor,
size : curSize
};
//force a dot to be drawn
self.move(id, 1, 0);
} else {
//this checks if the cusor size needs to be changed
var redraw = false;
for (i = 0; i < cursor.length; i++) {
if (x > cursor[i].location-25 && x < cursor[i].location+15) {
curSize = cursor[i].radius;
redraw = true;
}
}
if (redraw) self.drawCursors();

//if the color wheel is pressed
if (x > colorX-20 && x < colorX+40) {
self.showColors();
}

//check to see if the trashcan was clicked
if (x > trashX && x < trashX+20) {
var ck = confirm("Are you sure you want to clear your drawing?");
if (ck == true) {
lines = [];
ctxt.clearRect(drawingAreaX, drawingAreaY, drawingAreaWidth+10, drawingAreaHeight+10);
self.drawRect();
}
}
}
});
//prevent scrolling
event.preventDefault();
},
//when the screen is touched this where the drawing happens
draw: function(event) {
$.each(event.touches, function(i, touch) {
var x = this.pageX - offset.left,
y = this.pageY - offset.top;
//only store the keystrokes if they are in the drawing area
if (y < drawingAreaHeight && x < drawingAreaWidth) {
var id = touch.identifier,
moveX = x - lines[id].x,
moveY = y - lines[id].y,
ret = self.move(id, moveX, moveY);
lines[id].x = ret.x;
lines[id].y = ret.y;
}
});
//prevents scrolling
event.preventDefault();
},
//this is where the actual drawing on the screen happens
move: function(i, changeX, changeY) {
ctxt.strokeStyle = lines[i].color;
ctxt.lineWidth = lines[i].size;
ctxt.beginPath();
ctxt.moveTo(lines[i].x, lines[i].y);

ctxt.lineTo(lines[i].x + changeX, lines[i].y + changeY);
ctxt.stroke();
ctxt.closePath();

return { x: lines[i].x + changeX, y: lines[i].y + changeY };
}
};
//create the box to draw in
self.drawRect();
//now draw the cursors on screen and select the one in use
self.drawCursors();

//all the logic for the color picker
var colorWindow = {
init: function(imageObj) {
ctxtColor.strokeStyle = "#444";
ctxtColor.lineWidth = 2;
//draw the color picker image
ctxtColor.drawImage(imageObj, 0, 0);
colorWindow.drawColorSquare(curColor, imageObj);

//create the accept button
ctxtColor.beginPath();
ctxtColor.fillStyle = "grey";
var x = 15,
y = 340,
xS = 100,
yS = 40;
ctxtColor.fillRect(x, y, xS, yS);
ctxtColor.strokeRect(x, y, xS, yS);
ctxtColor.fillStyle = "black";
ctxtColor.font="20px Arial";
ctxtColor.fillText("Accept", x+20, 365);

//create the cancel button
ctxtColor.beginPath();
ctxtColor.fillStyle = "grey";
x += 125;
y += 0;
xS += 0;
yS += 0;
ctxtColor.fillRect(x, y, xS, yS);
ctxtColor.strokeRect(x, y, xS, yS);
ctxtColor.fillStyle = "black";
ctxtColor.font="20px Arial";
ctxtColor.fillText("Cancel", x+20, 365);

//set the fill style back so that when we try to color it's correct
ctxtColor.fillStyle = curColor;

canvasColor.addEventListener("touchstart", colorWindow.getColor, false);
canvasColor.addEventListener("touchmove", colorWindow.getColor, false);
},
//touch events
getColor: function(evt){
$.each(event.touches, function(i, touch) {
var x = this.pageX - offset.left,
y = this.pageY - offset.top,
padding = 20,
color = undefined;
//only get a color if it's over the color picker
if (x < imageObj.width+padding && x > padding &&
y < imageObj.height+padding && y > padding) {
//color picker image is 256x256 and is offset by 10px from top and bottom
var imageData = ctxtColor.getImageData(0, 0, imageObj.width, imageObj.width);
var data = imageData.data;
x -= padding
y -= padding
var red = data[((imageObj.width * y) + x) * 4];
var green = data[((imageObj.width * y) + x) * 4 + 1];
var blue = data[((imageObj.width * y) + x) * 4 + 2];
x += padding
y += padding

color = "rgb(" + red + "," + green + "," + blue + ")";
}
//if a color was selected then update the square
if (color) {
colorWindow.drawColorSquare(color, imageObj);
selColor = color;
}
//for pressing the buttons
if (evt.type == "touchstart" &&
x > 15 && x < 125 &&
y > 360 && y < 410)
{
curColor = selColor;
self.hideColors();
} else if (evt.type == "touchstart" &&
x > 150 && x < 275 &&
y > 360 && y < 410)
{
colorWindow.drawColorSquare(curColor, imageObj);
self.hideColors();
}
});
//prevent scrolling
event.preventDefault();
},
//draws/updates the square that shows the color selected from the picker
drawColorSquare: function(color, imageObj){
var colorRectX = 200,
colorRectY = 50,
squareX = 25,
squareY = 275;
ctxtColor.beginPath();
ctxtColor.fillStyle = color;
ctxtColor.fillRect(squareX, squareY, colorRectX, colorRectY);
ctxtColor.strokeRect(squareX, squareY, colorRectX, colorRectY);
}
};
//the color picker image
var imageObj = new Image();
imageObj.src = "color_picker.png";
imageObj.onload = function(){ colorWindow.init(this); };

//starts the drawing app
return self.init();
};

You’re going to want to follow the PhoneGap Android tutorial to help get you started on using Eclipse and getting an apk compiled for Android. I hope you enjoy what I’ve thrown together here and it will help you learn something. If any graphic artists out there want to donate some awesome graphics to improve my apps that would be greatly welcomed, although I’m not promising I’ll use anything thrown my way. And as always, feel free to leave questions and comments!

Tagged with: