iPhone Getting GPS & SDK Getting Maps API…?

Since I dissed the iPhone SDK earlier this month (iPhone SDK lacks mapping function), I wanted to pass on this update. Looks like a GPS-enabled iPhone is in the works for Apple’s upcoming World Wide Dev Conference. Even if the announcement doesn’t come then, looks like it will come soon according to EnGadget:

Second-gen iPhone: 3G, GPS, only slightly thicker

Click to see new GPS-iPhone at EndGadget

This is great news for mobile map-based application developers but don’t count on the API being as slick and open as Android’s. Given Apple’s approach so far, they’ll still withhold the vital functionality to get your app really humming.

Why do I say that?

Well for example, Apple’s SDK doesn’t allow applications to run in the background. That means death to any Pocket Journey app that would run on an iPhone as the audio/video would be killed along with your application as soon as someone called. Apple says this is for security reasons, but we know it’s due to Apple’s “control freakish” corporate nature. Happily for us, Android solved this challenge through their Service API.

Seems like Apple wants to step back to the dark ages of the Palm OS which can only handle single-threaded tasks as well

Android Tutorial #4.2: Passing custom attributes via XML resource files

In tutorial #4.1, I mentioned that we passed custom attributes for the text and image variables from the XML resource file to our custom class. This is a critical skill for performing true object-oriented programming and how to do it wasn’t obvious from Google’s Android API Demos.

Luckily I was pointed to the solution myself by an experienced Android programmer in Guatemala by the username of cadlg (thanks again!). If you want to see the official Google Android example though, look at Android’s APIDemos’ custom LabelView example.

So here we go. We’ll use the same code as tutorial 4.1 to keep this simple.

Setting Up Your Custom Class’s XML Resource Files

We’ll only review the code for the TextOnlyButton as it’s identical in concept to the ImageOnlyButton.

First we’ll create a new file in /res/values called attrs.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<resources>

<declare-styleable name=”TextOnlyButton”>

<attr name=”textColorNotFocused” format=”integer”/>
<attr name=”textColorFocused” format=”integer”/>

</declare-styleable>

</resources>

As you see, we first declared a ’styleable’ with the name of our custom Class. Two attributes were then added to contain the values of our focused & unfocused text colors. By default, attributes have values of String, but in our case, we needed integers to represent the resource id’s we’ll declare in our colors.xml file. You can also declare formats such as “boolean” & others if that suits the requirements of your own project.

Next, we declare values for these custom attributes in our layout’s XML file: tutorial4.xml

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”

xmlns:pj=”http://schemas.android.com/apk/res/com.pocketjourney.tutorials”
android:orientation=”vertical”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:padding=”10px”>

<com.pocketjourney.view.TextOnlyButton

android:id=”@+id/text_only_button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_marginTop=”5px”
style=”?android:attr/buttonStyleSmall”
android:text=”Text Button”
pj:textColorNotFocused=”@drawable/white”
pj:textColorFocused=”@drawable/android_orange”/>

</LinearLayout>

Referring to our new attributes is actually a two step process. First we declared a new namespace (in our case called ‘pj’ as short for PocketJourney) in the parent layout of our custom class:

xmlns:pj=”http://schemas.android.com/apk/res/com.pocketjourney.tutorials”

Next we specified the values of our new attributes in the XML usage of our TextOnlyButton:

pj:textColorNotFocused=”@drawable/white”
pj:textColorFocused=”@drawable/android_orange”

Now you can see why we specified our format=”integer”. Our custom attributes point to the resource id’s of colors specified in our colors.xml file.

Retrieving Custom Attributes During Class Instantiation

Since our Activity has many constructors, we delegate the attribute parsing to an init() method to keep our code clean.

int notFocusedTextColor, focusedTextColor;

private void init(AttributeSet attrs) {

Resources.StyledAttributes a = getContext().obtainStyledAttributes(attrs,R.styleable.TextOnlyButton);
notFocusedTextColor = a.getColor(R.styleable.TextOnlyButton_textColorNotFocused, 0xFF000000);
focusedTextColor = a.getColor(R.styleable.TextOnlyButton_textColorFocused, 0xFF000000);

}

By now you’ve undoubtedly seen the AttributeSet that is always passed into an Activity. Well now you get to use it. First we obtain the StyledAttributes instance by requesting just the StyledAttributes for our custom Class. Next, we call the getColor() and pass two variables: the name of the attribute we want along with a default value in case the user did not specify one.

Take note of our styled attribute’s name as it’s a combination of our custom class’s name and the attribute we specified in the attrs.xml file (e.g. TextOnlyButton_textColorNotFocused).

And That’s It

You can now readily pass your own custom attributes and keep your View variables cleanly enclosed in your XML files. You can download the source to see for yourself. Just look at Tutorial #4.

Prior Tutorials

Tutorial 1: Transparent Panel (Linear Layout) On MapView (Google Map)
Tutorial 2: “Hit” testing on a View (MapView)
Tutorial 3: Custom Media Streaming with MediaPlayer
Tutorial 4.1: Image and Text-Only Buttons

Android Tutorial #4.1: Image & Text-Only Buttons

This very simple tutorial will add to your Android UI (user interface) development arsenal. How? Buttons that display as simple text or as images are basic elements of any application. By following the two steps below, these buttons can be easily created with Google’s Android by simply extending the current Button and ImageButton classes.

We will create these two new Button extensions as shown:

Text and Image Buttons

Step 1: Override drawBackground()

This step shows the beauty of proper Object-Oriented programming. Because Google’s Android draws the background button image in a separate method, all we need do is override that method and then not call its parent.

public void onDrawBackground(Canvas canvas) {

// Override this method & do nothing. This prevents the parent.onDrawBackground(canvas)
// from drawing the button’s background.

}

Step 2: Handle onFocus() by overriding onDraw()

We listen for the focus event and update the Button’s text color or button image that way as well. But we’ll do it this way for now.

public void onDraw(Canvas canvas) {

// Since this Button now has no background. We must set the text color to indicate focus.
if (isFocused()) {

// Set the focused text color. In the case of ImageOnlyButton we would .
// instead do setImageResource(imageResourceFocused);
setTextColor(focusedTextColor);

} else {

// Set the non-focused text color. In the case of ImageOnlyButton we would .
// instead do setImageResource(imageResourceNotFocused);
setTextColor(notFocusedTextColor);

}

super.onDraw(canvas);

}

You might be wondering where those text color and image variables come from. We will obtain these focused & not-focused images or text colors from variables passed in from the XML resource file as you’ll see by reading the source code. I won’t go into how that happens here. Instead, I’ll keep this tutorial simple and point you to my next tutorial where I will discuss how to create & pass such custom variables.

And That’s It

You can download the source to see for yourself.

Prior Tutorials

Tutorial 1: Transparent Panel (Linear Layout) On MapView (Google Map)
Tutorial 2: “Hit” testing on a View (MapView)
Tutorial 3: Custom Media Streaming with MediaPlayer

iPhone SDK lacks mapping function (Android doesn’t of course)

Here’s a quick insight from research into the iPhone SDK in comparison to the Android OS.

Yes…you read this post’s title correctly. The iPhone SDK lacks the basic mapping API required by applications developers hoping to tie the mobile experience to a geo-specific location (GPS). The only GPS mapping function provided by the iPhone SDK is to send a URL to the iPhone’s built-in Google map application and to let Google handle the rest.

Curiously the iPhone SDK does provide a basic location service API providing the user’s location using cell signal triangulation, but without a map, this information is all-but-useless.

At first glance you might say, “that makes sense as the iPhone doesn’t have GPS built-in because it would have raised the price of an already expensive phone.”

Point taken but all this leaves me to believe that Apple’s take on the iPhone is too media focused and lacks clear understanding of the power provided by location-awareness. That’s great news for the upcoming GPS-enabled Android phones. They’ll have time to widely proliferate before the soon-to-be almost 10M iPhones out there will be replaced with GPS-enabled iPhones.

Imagine if Android had lacked the MapActivity API…?!? From my count, 90% of the most interesting Android Challenge entries would never exist. Without Android’s MapActivity API, we would be left with a few accelerometer-based games and non-SMS messaging apps.

This is great news for Google and perhaps that’s the point. Google wrote the iPhone’s map application and was either not given incentive to make expose its iPhone map through an API or perhaps decided it was smart to keep the iPhone limited in this crucial manner. OK…maybe Apple has a bigger plan as it always does. Maybe Apple is preparing to launch its own mapping application or perhaps AT&T didn’t want Apple to expose such location-based apps because AT&T has its own GPS agenda.

Thoughts anyone?

Android Video & Screenshots Released

We were inspired by all the great videos presenting other Android-enabled applications being entered into Google’s Android Challenge so we created a short (4 minute) video about Pocket Journey.

Video Splash

You’ll be shown screen shots in the video or you can view close ups on our main website

Pocket Journey home screen

We’ve tried to capture what we view as the power of community and the interest we’ve heard around packaging high quality knowledge from a range of people and making it available through mobile phones.

Everyone\'s Knowledge

As you can imagine, there will be a broad range of topics provided through Pocket Journey so it will be a major challenge to filter, pacakge, & deliver just the information you want.

A world of knowledge

Yes…this GREATLY simplifies the challenge but we wanted to give you a very very “rough” idea of where we’re headed. We plan to provide more details as the project matures.

How Pocket Journey works - greatly simplified

But don’t just take these static images as the real message. Check out the Pocket Journey video to get a much clearer understanding and to actually “hear” what it’s all about. It will only take 4 minutes of your time.

Android Tutorial 3: Custom Media Streaming with MediaPlayer

Introduction

This is a long tutorial, but for those of you that have been struggling with streaming of media to Google’s Android’s MediaPlayer, then I hope this tutorial proves useful as you finalize your entries into Google’s Android Challenge

This tutorial will show how to roll your own streaming media utility for Android’s MediaPlayer. We will buffer 10 seconds of audio and start playing that audio while the rest of the audio loads in the background. We store the streamed audio locally so you could cache it on device for later use or simply let it be garbage collected.

Here’s the source code for those that just want to jump in. You’ll also notice code for the other tutorials as I didn’t have time to strip them out.

Here are a few screenshots of what we’ll be creating:

Tutorial #3 results screenshots

Basic Layout

The tutorial consists of just two classes:

Tutorial3: Contains the UI layout and process button clicks
StreamingMediaPlayer: Connects to the server, downloads audio into the buffer, and controls the functionality to ensure the audio continues to play seamlessly.

We’ll assume you know about UI layout using Android’s XML resource files and will instead jump right into the audio streaming code.

Start Your Streaming

Upon clicking the “Start Streaming” button, Tutorial3 creates an instance of StreamingMediaPlayer.

new StreamingMediaPlayer(textStreamed, playButton, streamButton,progressBar);

All UI elements are passed to StreamingMediaPlayer so it can perform UI update itself. In a more robust implementation, StreamingMediaPlayer would fire relevant update events and Tutorial3 would handle the UI updates. For simplicity & cleaner code in this tutorial however, StreamingMediaPlayer will be directly updating the UI.

Tutorial3 then calls StreamingMediaPlayer.startStreaming():

audioStreamer.startStreaming(”http://www.pocketjourney.com/audio.mp3″,1444, 180);

Three variables are passed to startStreaming(): a url for the media to stream (link to an .mp3 file in this tutorial), the length in kilobytes of the media file, and the lenght in seconds of the media file. These last two values will be used when updating the progress bar.

AudioStreamer.startStreaming() creates a new thread for streaming the content so we can immediately return control back to the user interface.

public void startStreaming(final String mediaUrl, long mediaLengthInKb, long mediaLengthInSeconds) throws IOException {

this.mediaLengthInKb = mediaLengthInKb;
this.mediaLengthInSeconds = mediaLengthInSeconds;

Runnable r = new Runnable() {

public void run() {

try {

downloadAudioIncrement(mediaUrl);

} catch (IOException e) {

Log.e(getClass().getName(), “Initialization error for fileUrl=” + mediaUrl, e);
return;

}

}

};
new Thread(r).start();

}

Incremental Media Download

This is where the magic happens as we download media content from the the url stream until we have enough content buffered to start the MediaPlayer. We then let the MediaPlayer play in the background while we download the remaining audio. If the MediaPlayer reaches the end of the buffered audio, then we transfer any newly downloaded audio to the MediaPlayer and let it start playing again.

Things get a little tricky here because:

(a) The MediaPlayer seems to lock the file so we can’t simply append our content to the existing file.
(b) Pausing the MediaPlayer to load the new content takes awhile so we only want to interrupt it when absolutely necessary.
(c) Accessing the MediaPlayer from a separate thread causes it to crash.

So with those caveats in mind, here’s the method that bufferes the media content to a temporary file:

public void downloadAudioIncrement(String mediaUrl) throws IOException {

// First establish connection to the media provider
URLConnection cn = new URL(mediaUrl).openConnection();
cn.connect();
InputStream stream = cn.getInputStream();
if (stream == null) {

Log.e(getClass().getName(), “Unable to create InputStream for mediaUrl:” + mediaUrl);

}

// Create the temporary file for buffering data into
downloadingMediaFile = File.createTempFile(”downloadingMedia”, “.dat”);
FileOutputStream out = new FileOutputStream(downloadingMediaFile);

// Start reading data from the URL stream
byte buf[] = new byte[16384];
int totalBytesRead = 0, incrementalBytesRead = 0;
do {

int numread = stream.read(buf);
if (numread <= 0) {

// Nothing left to read so quit
break;

} else {

out.write(buf, 0, numread);
totalBytesRead += numread;
incrementalBytesRead += numread;
totalKbRead = totalBytesRead/1000;

// Test whether we need to transfer buffered data to the MediaPlayer
testMediaBuffer();

// Update the status for ProgressBar and TextFields
fireDataLoadUpdate();

}

} while (true);

// Lastly transfer fully loaded audio to the MediaPlayer and close the InputStream
fireDataFullyLoaded();
stream.close();

}

What’s up with testMediaBuffer()?

So if you were paying attention, an important piece of functionality must reside in the testMediaBuffer() method. You’re right. That’s the method where we determine whether we need to transfer buffered data to the MediaPlayer because we have enough to start the MediaPlayer or because the MediaPlayer has already played out its previous buffer content.

Before we jump into that, please take note that interacting with a MediaPlayer on non-main UI thread can causes crashes to we always ensure we are interacting on the main-UI Thread by using a Handler when necessary. For example, we must do so in the following method because it is being called by the media streaming Thread.

private void testMediaBuffer() {

// We’ll place our following code into a Runnable so the Handler can call it for running
// on the main UI thread

Runnable updater = new Runnable() {

public void run() {

if (mediaPlayer == null) {

// The MediaPlayer has not yet been created so see if we have
// the minimum buffered data yet.
// For our purposes, we take the minimum buffered requirement to be:
// INTIAL_KB_BUFFER = 96*10/8;//assume 96kbps*10secs/8bits per byte

if ( totalKbRead >= INTIAL_KB_BUFFER) {

try {

// We have enough buffered content so start the MediaPlayer
startMediaPlayer(bufferedFile);

} catch (Exception e) {

Log.e(getClass().getName(), “Error copying buffered conent.”, e);

}

}

} else if ( mediaPlayer.getDuration() - mediaPlayer.getCurrentPosition() <= 1000 ){

// The MediaPlayer has been started and has reached the end of its buffered
// content. We test for < 1second of data (i.e. 1000ms) because the media
// player will often stop when there are still a few milliseconds of data left to play

transferBufferToMediaPlayer();

}

}

};
handler.post(updater);

}

Starting the MediaPlayer with Initial Content Buffer

Starting the MediaPlayer is very straightforward now. We simply copy all the currently buffered content
into a new Ffile and start the MediaPlayer with it.

private void startMediaPlayer(File bufferedFile) {

try {

File bufferedFile = File.createTempFile(”playingMedia”, “.dat”);
FileUtils.copyFile(downloadingMediaFile,bufferedFile);
} catch (IOException e) {

mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(bufferedFile.getAbsolutePath());
mediaPlayer.prepare();
fireDataPreloadComplete();

Log.e(getClass().getName(), “Error initializing the MediaPlaer.”, e);
return;

}

}

Transferring Buffered Content to a MediaPlayer That is Already Playing

This is a little trickier but not much. We simply pause the MediaPlayer if it was playing (i.e. the user had not pressed pause), copy over the currently downloaded media content (which may be all of it by now) and then restart the MediaPlayer if it was previously running or had hit the end of its buffer due to a slow network.

private void transferBufferToMediaPlayer() {

try {

// Determine if we need to restart the player after transferring data (e.g. perhaps the user
// pressed pause) & also store the current audio position so we can reset it later.

boolean wasPlaying = mediaPlayer.isPlaying();
int curPosition = mediaPlayer.getCurrentPosition();
mediaPlayer.pause();

// Copy the current buffer file as we can’t download content into the same file that
// the MediaPlayer is reading from.

File bufferedFile = File.createTempFile(”playingMedia”, “.dat”);
FileUtils.copyFile(downloadingMediaFile,bufferedFile);

// Create a new MediaPlayer. We’ve tried reusing them but that seems to result in
// more system crashes than simply creating new ones.

mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(bufferedFile.getAbsolutePath());
mediaPlayer.prepare();
mediaPlayer.seekTo(curPosition);

// Restart if at end of prior beuffered content or mediaPlayer was previously playing.
// NOTE: We test for < 1second of data because the media player can stop when there is still
// a few milliseconds of data left to play

boolean atEndOfFile = mediaPlayer.getDuration() - mediaPlayer.getCurrentPosition() <= 1000;
if (wasPlaying || atEndOfFile){

mediaPlayer.start();

}

}catch (Exception e) {

Log.e(getClass().getName(), “Error updating to newly loaded content.”, e);

}

}

Conclusion

To get the real feel for how your audio will download, make sure to set it to a slower network speed. I recommend setting to AT&T’s EDGE network setting as it should give a lower limit on expected performance. You can make these setting’s easy in Eclipse by setting going into your Run or Debug setting’s dialog and making these selections.

EDGE settings in Eclipse

Well that’s it. I’ve inluded additional code for handling the ProgressBar and TextField updates but that should all be sufficiently easy to understand once you understand the rest of the code. Good luck during the next week as you finish your Android Challenge submissions.

And of course, here’s the source code. Please post a comment below if I need to explain anything in more detail. You’ll also notice code for the other tutorials as I didn’t have time to strip them out.

Android Tutorial 2: “Hit” testing on a View (MapView)

The following tutorial addresses how to perform ‘hit’ testing for user ‘clicks’ in a View. By hit testing, we mean the ability to determine when a user’s selection of a specific Point in a View overlaps with a region that we are monitoring for further action.

In other words at the end of this MapView tutorial, your users will be able to click on any icon that you draw onto the map, and you’ll be able to take whatever action you like such as displaying a transparent popup window (as we do in the tutorial).

Here’s what the final result will look like:

Screenshot of Tutorial 2 results

We’ll assume you already know how to add a MapView to a layout and create an Overlay and will jump right into how to test whether a user selection ‘hit’ one of those mapped icons.

Our icons are rendered by an extension of Overlay we have named MapLocationOverlay which has 2 primary methods called during an Overlay.draw(). We’ll go through each of these in detail:

drawMapLocations(canvas, calculator, shadow);
drawInfoWindow(canvas, calculator, shadow);

More important perhaps, we’ll discuss the following method which performs the hit testing of each user tap on the screen.

isHitMapLocation(calculator,point);

Starting with locations on our MapView

We start by creating a class, MapLocation, to store our map location name, latitude, & longitude. Four instances of MapLocation are created as shown in the screenshot of San Francisco above:

mapLocations = new ArrayList<MapLocation>();
mapLocations.add(new MapLocation(”North Beach”,37.799800872802734,-122.40699768066406));
mapLocations.add(new MapLocation(”China Town”,37.792598724365234,-122.40599822998047));
mapLocations.add(new MapLocation(”Fisherman’s Wharf”,37.8091011047,-122.416000366));
mapLocations.add(new MapLocation(”Financial District”,37.79410171508789,-122.4010009765625));

These map locations will be drawn to the MapView and used for testing user clicks.

Drawing Map Locations

Before we can test for users clicking our icon, we need to first learn how those icons are drawn to the screen. Once you are comfortable setting the screen coordinates for drawing of your icon, it will be simple to test those same coordinates for ‘hits’ (user clicks” on that icon).

Screen coordinates start at (0,0) in the upper-left and end at the bottom-right (screenWidth,screenHeight) of our screen. To draw our location’s icon, we must first know how to translate from the latitude/longitude coordinates of our map location to these x & y screen coordinates. Android provides this function for us via a PixelCalculator which is passed in Overlay’s draw() method.

public void draw(Canvas canvas, PixelCalculator calculator, boolean shadow)

To use PixelCalculator, simply pass an int[2] to the calculator along with our location’s latitude/longitude as a Point. The calculator does its magic and returns the screen coordinates of our map location.

int[] screenCoords = new int[2];
calculator.getPointXY(testLocation.getPoint(), screenCoords);

As we will be drawing a bitmap balloon icon to the screen, we must ensuring that the bottom middle of our icon is directly on top of our location’s latitude & longitude screen coordinates (as shown in the image below). This accurate positioning of our icon will be key to hit testing later on.

Screen coordinates of icon

To draw the balloon icon then, we call drawBitmap() and ensure the top/left of our icon is properly offset.

canvas.drawBitmap(icon, screenCoords[0] - icon.width()/2, screenCoords[1] - icon.height(),null);

And that’s it, our icons is now properly drawn on the screen. Now we need to perform ‘hit’ tests for user interaction with these icons.

Listening for Map Taps & Then Testing for ‘Hits’

User taps on the MapView are captured by overriding Overlay’s onTap() method and then testing for overlap with our icons’ locations on the screen. If a hit occurs and new information popup displayed (or a prior information popup removed), then we invalidate the map so Overlay.draw() is called.

@Override
public boolean onTap(DeviceType deviceType, Point p, PixelCalculator calculator) {

// Store whether prior popup was displayed so call invalidate() to remove it if necessary.
boolean isRemovePriorPopup = selectedMapLocation != null;

// Next test whether a new popup should be displayed
selectedMapLocation = getHitMapLocation(calculator,p);
if ( isRemovePriorPopup || selectedMapLocation != null) {

mapView.invalidate();

}

// Lastly return true if we handled this onTap()
return selectedMapLocation != null;

}

So here’s the real point of this tutorial…how do we match the screen coordinates that the user clicks to the latitude & longitude of our icon on the map?

Just as we determined the location of our map for drawing on the screen, we will now create a Rectangle to represent that drawn icon and use the Rectangle.contains() method to test whether the user’s MotionEvent occurred within that Rectangle.

private MapLocation getHitMapLocation(PixelCalculator calculator, Point tapPoint) {

// Track which MapLocation was hit…if any
MapLocation hitMapLocation = null;

RectF hitTestRecr = new RectF();
int[] screenCoords = new int[2];
Iterator<MapLocation> iterator = mapView.getMapLocations().iterator();
while(iterator.hasNext()) {

MapLocation testLocation = iterator.next();

// As above, translate MapLocation lat/long to screen coordinates
calculator.getPointXY(testLocation.getPoint(), screenCoords);

// Use this information to create a ‘hit” testing Rectangle to represent the size
// of our location’s icon at the correct location on the screen.

// As we want the base of our balloon icon to be at the exact location of
//
our map location, we set our Rectangle’s location so the bottom-middle of
//
our icon is at the screen coordinates of our map location (shown above).
hitTestRecr.set(-bubbleIcon.width()/2,-bubbleIcon.height(),bubbleIcon.width()/2,0);

// Next, offset the Rectangle to location of our location’s icon on the screen.
hitTestRecr.offset(screenCoords[0],screenCoords[1]);

// Finally test for match between ‘hit’ Rectangle and location clicked by the user.
// If a hit occurred, then we stop processing and return the result;

calculator.getPointXY(tapPoint, screenCoords);
if (hitTestRecr.contains(screenCoords[0],screenCoords[1])) {

hitMapLocation = testLocation;
break;

}

}

return hitMapLocation;

}

And that’s it for hit testing. If a hit occurred in our Rectangle, we track the selected map location and render a popup window above the map location’s icon with the name of the location.

Drawing a Popup Information Window

The following code for displaying a popup window may look complex, but the goal is simple - to set the correct screen coordinates for the information window to display directly above & centered on our location’s icon.

private void drawInfoWindow(Canvas canvas, PixelCalculator calculator, boolean shadow) {

// Again get our screen coordinate
int[] selDestinationOffset = new int[2];
calculator.getPointXY(selectedMapLocation.getPoint(), selDestinationOffset);

// Setup the info window with the right size & location
int INFO_WINDOW_WIDTH = 125;
int INFO_WINDOW_HEIGHT = 25;
RectF infoWindowRect = new RectF(0,0,INFO_WINDOW_WIDTH,INFO_WINDOW_HEIGHT);
int infoWindowOffsetX = selDestinationOffset[0]-INFO_WINDOW_WIDTH/2;
int infoWindowOffsetY = selDestinationOffset[1]-INFO_WINDOW_HEIGHT-bubbleIcon.height();
infoWindowRect.offset(infoWindowOffsetX,infoWindowOffsetY);

// Draw inner info window
canvas.drawRoundRect(infoWindowRect, 5, 5, getInnerPaint());

// Draw border for info window
canvas.drawRoundRect(infoWindowRect, 5, 5, getBorderPaint());

// Draw the MapLocation’s name
int TEXT_OFFSET_X = 10;
int TEXT_OFFSET_Y = 15;
canvas.drawText(selectedMapLocation.getName(),infoWindowOffsetX+TEXT_OFFSET_X,infoWindowOffsetY+TEXT_OFFSET_Y,getTextPaint());

}

And that’s it. Please let me know of any points that need clarification or that I should expand/improve upon.

Here is the .apk you can use along with the source files: tutorial2.zip.

Happy coding,

Anthony (Acopernicus)

Tutorial 1: Transparent Panel (Linear Layout) On MapView (Google Map)

This tutorial is for Google’s Android mobile operating system. If you haven’t already heard about Android, then check it out immediately because it’s way cool. We have benefited so much from the Android developer community that we want to give back our own insights into the platform and how to better design/develop on the platform.

For this tutorial, we’re going to help the several people that have asked us how to create transparent panels. While we show how to overlay onto a Google Map, you can use the same technique to overlay a transparent panel onto any other view.

Starting at the end, this what we’ll develop today - a transparent panel with a single button displayed at the base of an Android MapView

Tutorial 1 - final result

Tutorial 1 - final result (closeup)

We’ll assume that you already know the basics of Android programming and will only address these “advanced” topics:

1) Creating a class that can draw a transparent background and border.
2) Adding a custom View class as a declaration in your layout XML.

(1) Creating a Custom Layout as a Transparent Panel

We wanted our transparent panel to hold children so we looked for the most natural Android view to extend and selected Linear Layout because we wanted our TransparentPanel class to layout its children horizontally. We could just as easily chosen to extend RelativeLayout of any other layout class.

TransparentPanel extends LinearLayout

The ‘magic’ of TransparentPanel happens in the dispathDraw() method. For those of you that have already created their own custom Views, you might wonder why we override dispatchDraw() instead of onDraw(). For some reason, LinearLayout does not call it’s own onDraw() method…apparently because its developer assumes a LinearLayout would never have anything to draw. BUT we want our TransparentPanel to draw a background so we override dispatchDraw() to draw the background and then let super.dispatchDraw(canvas) render any child components.

protected void dispatchDraw(Canvas canvas) {

RectF drawRect = new RectF();
drawRect.set(0,0, getMeasuredWidth(), getMeasuredHeight());

canvas.drawRoundRect(drawRect, 5, 5, innerPaint);
canvas.drawRoundRect(drawRect, 5, 5, borderPaint);

super.dispatchDraw(canvas);

}

For those new to drawing their own graphics, let’s review a few items here. First, we populate a RectF with the coordinates of the background that we want to draw. Next we make to calls to drawRoundRect(). The 1st call passes innerPaint to draw the transparent gray background while the 2nd call passes the white border that we want to paint. Lastly we call super.dispatchDraw(canvas) to render our child components (in this case a Button).

The gray background is rendered with an alpha (transparency ) == 225. This allows just enough of the map to show through.

innerPaint.setARGB(225, 75, 75, 75);

And borderPaint allows us to render a white border with a Stroke of width = 2.

borderPaint = new Paint();
borderPaint.setARGB(255, 255, 255, 255);
borderPaint.setAntiAlias(true);
borderPaint.setStyle(Style.STROKE);
borderPaint.setStrokeWidth(2);

As we did above, make sure to setAntiAlias(true) so the borders of your paint blend seamlessly with its surroundings. Set this option to false to see how your borders would have jagged edges otherwise.

(2) Adding our custom TransparentPanel class as a declarations in the layout XML.

Now we’re ready to insert the TransparentPanel into our layout XML class and to add a button. To reference our new class, we simply provide the full classpath to our the TransparentPanel and then provide layout parameters as we would for any LinearLayout. In this case, we provide padding so our Button does not rest against the edges of our TransparentPanel’s border.

<com.pocketjourney.view.TransparentPanel

android:id=”@+id/transparent_panel”
android:layout_width=”fill_parent”
android:layout_height=”wrap_content”
android:paddingLeft=”5px”
android:paddingTop=”5px”
android:paddingRight=”5px”
android:paddingBottom=”5px”>

<Button android:id=”@+id/button_click_me”

android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Click Me!”>

</com.pocketjourney.view.TransparentPanel>

And that’s it. Here is the .apk you can use along with the source files: tutorial1.zip.

Please give us your feedback and let us know any suggestions for improving this tutorial.

Welcome to Pocket Journey

We’ve been working towards this release for over a year now and the pieces are finally coming together. Our ‘official’ website has gone into public stealth mode…yes that’s a little cryptic but we guarantee your wait will be worthwhile.

In the meantime, please read on and explore this site. There will be enough leaked knowledge to wet your appetite for the final launch which we hope will arrive by year’s end. We’re participating in Google’s Android Challenge and will be releasing screenshots and more insights into Audible Journey over the next few months. If you haven’t heard about Android but like the vision of Pocket Journey, then you’re going to be in for a shock. We were. None of us expected so much progress on the mobile front in such a short time. Judging from the just announced release of Apple iPhone SDK, we’re about to experience a MAJOR shift in what we will expect from our cell phones over the next few years.

Please feel free to post your own thoughts about Pocket Journey or to contact us. Without community participation both on the business development & the member usage side, this vision will never become a reality.