I write solutions to the problems I can't find much about elsewhere on the Web, also some code/script snippets that are absolutely awesome and make my life easier. Will be glad if someone finds these posts interesting and helpful!

Friday, September 23, 2011

Calculate distance between two points with MySQL

This function here is the right one. It returns pretty same results as the Google Maps distance, which leads me to believe that it's kind of correct.

CREATE FUNCTION distance_between (from_lat DECIMAL(6, 3), from_lng DECIMAL(6, 3), to_lat DECIMAL(6, 3), to_lng DECIMAL(6, 3)) RETURNS DECIMAL(11, 3)
RETURN 6371 * 2 * ATAN2(SQRT(POW(SIN(RADIANS(to_lat - from_lat)/2), 2) + POW(SIN(RADIANS(to_lng - from_lng)/2), 2) * COS(RADIANS(from_lat)) * COS(RADIANS(to_lat))), SQRT(1 - POW(SIN(RADIANS(to_lat - from_lat)/2), 2) + POW(SIN(RADIANS(to_lng - from_lng)/2), 2) * COS(RADIANS(from_lat)) * COS(RADIANS(to_lat))));

I've seen a lot of ready solutions online for the same input/output/platform combination (i.e. WGS84 degrees input/output on MySQL), but all of them used some other formula (at the end multiplying by 1.1515) that was producing some results that never worked for me.

On the other hand the following formula returns results identical with what Google Maps returns.
My function output

Cross check
Apparently there's some issue with the bearing and that's why the result is still not precisely accurate, but it's much closer than the other methods' outputs I've tried before.


Tuesday, June 7, 2011

Offline tiled layer with ArcGIS for Android

On the official ArcGIS mobile blog they have described how to create an offline tiled layer for iOS, but not a word about Android (probably because it's still in beta, thing might change and it doesn't gain any publicity anyway). The idea was taken from this blog: if not for that, I would've not even attempted to look into this direction. So thank God for a custom hack!
My first attempt for offline tiles was to create a local tile server and in the ArcGISTiledMapServiceLayer as a URL supply something with localhost in it. It totally worked, but there were two issues that forced me to look for another solution. Number one was technical: as soon as the connection was down (e.g. Airplane mode turned on) the tiles would just stop to download (even though their physical location was on the same SD card the application was installed on!); the second issue was mental: just for knowing that the files stored locally had to take a trip to the moon before being rendered (let alone maintaining a whole process of a local map server), caused an allergy and made me invest some time into research.
So here we go with offline tiled layer. Easier than easy, with a very little code, but totally impossible to find out how to make it! At least at the current version of ArcGIS for Android all the interesting stuff regarding custom tiled layers is undocumented.
Here's the implementation of the custom tiled layer:
package com.aploon.map.esri;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonParser;
import android.content.Context;
import android.util.AttributeSet;
import com.esri.android.map.TiledLayer;
import com.esri.core.map.TiledLayerModel;
public class OfflineTiledLayer extends TiledLayer {
public OfflineTiledLayer(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
}
public OfflineTiledLayer(Context paramContext,
AttributeSet paramAttributeSet, int paramInt) {
super(paramContext, paramAttributeSet, paramInt);
}
File workingDirectory;
String relativePath;
String tileServerParamJsonPath;
String tilesPath;
public OfflineTiledLayer(Context paramContext, File workingDirectory, String relativePath, String tileServerParamJsonPath, String tilesPath) {
super(paramContext, null);
this.workingDirectory = workingDirectory;
this.tileServerParamJsonPath = tileServerParamJsonPath;
this.relativePath = relativePath;
this.tilesPath = tilesPath;
}
protected TiledLayerModel initModel() throws Exception {
//read into JsonParser the specification of this map server (spatial reference, full extent, image parameters etc)
JsonParser paramJsonParser = new JsonFactory().createJsonParser(new File(workingDirectory.getAbsolutePath() + relativePath + tileServerParamJsonPath));
paramJsonParser.nextToken();//advance the json parser to the beginning, because the next call will check whether cursor is in the start_object or not, and if not it will return null
//this call actually parses the json specification into a MapServer instance (it's just a bean holding values)
com.esri.core.internal.d.g mapServer = com.esri.core.internal.d.g.a(paramJsonParser, null);
//create the model; spatial reference and envelope were somewhat easy to find out, but TileInfo is an internal class
// spatial ref envelope tileinfo
model = new TiledLayerModel(mapServer.g(), mapServer.k(), mapServer.i()) {
private static final long serialVersionUID = 1L;
/**
* This is the main method. It returns the tile as an array of bytes for requested level, row, column.
*/
public byte[] getTile(int lev, int row, int col) throws Exception {
File requestedFile = new File(workingDirectory.getAbsolutePath() + relativePath + tilesPath + lev + "/" + row + "/" + col);
FileInputStream requestedFileStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {//attempt to read the file requested and write it to the output
requestedFileStream = new FileInputStream(requestedFile);
int count = 8192;
int offset = 0;
byte[] buffer = new byte[count];
int length = -1;
while((length = requestedFileStream.read(buffer, offset, count)) > -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
}
catch(Exception e) {
return new byte[0];
}
finally {//close streams
if(outputStream != null) {
outputStream.close();
}
if(requestedFileStream != null) {
requestedFileStream.close();
}
}
}
};
//set initial extent to the model
// extent spatial ref
((TiledLayerModel) model).setExtent(mapServer.j(), 400, 400, mapServer.l());
return (TiledLayerModel) model;
}
}
Here's a usage example:
map = (MapView) findViewById(R.id.map); //get the map instance
TiledLayer DAY_LAYER = new OfflineTiledLayer(this, new File(Environment.getExternalStorageDirectory(), "services"), "/RoadMapsWebMercator101010/MapServer/", "index.html", "/Day/tile/"); //create a layer
map.addLayer(DAY_LAYER); //add this layer to the map
view raw gistfile1.java hosted with ❤ by GitHub

Now about the filesystem. In the usage example I have set some values to the OfflineTiledLayer constructor and these represent the directory structure. First off, I didn't use the cache created by ArcGIS. I mentioned in the beginning of the post that earlier I had implemented a local map server, that's why the file paths reflect the ones used by the online map servers. For instance, the absolute path of the tile at zoom level 3/row 44/column 65 looks like /mnt/sdcard/services/RoadMapsWebMercator101010/MapServer/tile/3/44/65. I guess it's not hard at all to modify my class to use the HEX paths of the cache created by ArcGIS.
Finally, a couple words about the map server specification. That paramter index.html of the OfflineTiledLayer constructor represents this. Again, I named it index.html to support the local map server implemented earlier, but in fact it contains what an online map server outputs by clicking on the link "REST" on the bottom of the MapServer description page of the ArcGIS server (the address looks something like /MapServer?f=json&pretty=true). 

And that's totally it. Hope it helps!

Friday, May 6, 2011

Cloudmade routing on OSMDroid

Another mystery without much relevant results on Google search that turned out to be quite an easy task.
Cloudmade seems to make a lot of effort to provide developers with a lot of mapping features, and as I haven't found anywhere on the site how to pay them, I take it that their service is free. It seems also that they use OpenStreetMaps as a back-end, therefore (here goes the disclaimer) ROUTING IS NOT VERY ACCURATE. At least not yet, but it's being improved a lot; I've added a Starbucks shop about a month ago, and now it's visible on the map!
Among other services Cloudmade offers Routing HTTP API, which seems to be a breeze for use within HTML code (they support JSONP-style callback for script injection to go around cross-origin resource sharing restrictions). This is the one to be used in the open map view with open map controller, because Google Maps doesn't allow routing anywhere outside their domain (Google even removed routing API from Android right after the very first release!).
My implementation of the BlueLine does everything from requesting directions, all the way to drawing them on the map.
Create a class extending PathOverlay with the following code:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import javax.xml.parsers.SAXParserFactory;
import org.osmdroid.tileprovider.util.CloudmadeUtil;
import org.osmdroid.views.overlay.PathOverlay;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import android.content.Context;
import android.graphics.Point;
public class BlueLine extends PathOverlay {
String CMURL;
public BlueLine(int lineColor, Context ctx) {
super(lineColor, ctx);
getPaint().setStrokeWidth(5f);
CloudmadeUtil.retrieveCloudmadeKey(ctx);
//prebuild the URL http://routes.cloudmade.com/YOUR-API-KEY-GOES-HERE/api/0.3/PARAMS-GO-HERE
CMURL = "http://routes.cloudmade.com/" + CloudmadeUtil.getCloudmadeKey() + "/api/0.3/";
}
double endLat, endLng;
public void showDirectionsToPredefinedLocation(double startLat, double startLng, boolean shortest, String lang, boolean metric) throws Exception {
showDirections(startLat, startLng, endLat, endLng, shortest, lang, metric);
}
/**
* Request directions from a point to another point.
* Line is drawn automatically in the draw method, need to just request directions and parse the XML into the List of Point's.
* @param startLat Latitude of starting point
* @param startLng Longitude of starting point
* @param endLat Latitude of ending point
* @param endLng Longitude of ending point
* @param shortest Whether select shortest or fastest route
* @param lang 2-char ISO code of the language of directions
* @param metric Whether use metric or imperial system
* @throws Exception
*/
public void showDirections(double startLat, double startLng, double endLat, double endLng, boolean shortest, String lang, boolean metric) throws Exception {
/*
* Query up the routing service to figure out the turn points
* documentation for cloudmade routing service http://developers.cloudmade.com/wiki/routing-http-api/Documentation
*/
//add PARAMS to the URL
//start_point,[transit_point1,...,transit_pointN],end_point/route_type[/route_type_modifier].output_format[?lang=(Two letter ISO 3166-1 code)][&units=(km|miles)]
CMURL += startLat + "," + startLng + "," + endLat + "," + endLng
+ "/car/" + (shortest? "shortest" : "fastest") + ".gpx?lang=" + lang + "&units=" + (metric? "km" : "miles");
//fetch directions and parse returned XML
String directionsXML = getContent(CMURL, null, "UTF8");
//parse XML
TurnPointsParser parser = new TurnPointsParser();
XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
xmlReader.setContentHandler(parser);
if(!"".equals(directionsXML)) {
xmlReader.parse(new InputSource(new StringReader(directionsXML)));
}
}
public class TurnPointsParser extends DefaultHandler {
public static final String ROOT_ELEMENT = "wpt";
ArrayList<Point> tempPoints = new ArrayList<Point>();
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
if(qName.equals(ROOT_ELEMENT)) {
try {
int late6 = degreesToMicrodegrees(Double.parseDouble(atts.getValue("lat")));
int lnge6 = degreesToMicrodegrees(Double.parseDouble(atts.getValue("lon")));
tempPoints.add(new Point(late6, lnge6));
}
catch(Exception e) {
e.printStackTrace();
}
}
}
@Override
public void endDocument() {
setPoints(tempPoints);
}
}
void setPoints(ArrayList<Point> points) {
clearPath();
for(Point p : points) {
addPoint(p.x, p.y);
}
}
/**
* Fetch the content of service at the given URL with provided parameters.
* Also transcode the input into UTF-8 according to specified charset used by the service.
* @param serviceUrl
* @param requestParams
* @param serviceCharset
* @return String with fetched content
* @throws Exception
*/
public static String getContent(String serviceUrl, String requestParams, String serviceCharset) throws Exception {
URL service = new URL(serviceUrl);
URLConnection serviceConnection = service.openConnection();
serviceConnection.setDoOutput(true);
//write request to the connection
OutputStreamWriter request = new OutputStreamWriter(serviceConnection.getOutputStream());
if (requestParams != null) {
request.write(requestParams);
}
request.flush();
//read returned output
BufferedReader in =
new BufferedReader(new InputStreamReader(serviceConnection.getInputStream(), serviceCharset));
String inputLine;
String returnedContent = "";
while ((inputLine = in.readLine()) != null) {
returnedContent += inputLine;
}
in.close();
request.close();
return returnedContent;
}
public static final double MICRODEGREES_COEFF = 1E6;
public static int degreesToMicrodegrees(double degrees) {
return (int) (degrees * MICRODEGREES_COEFF);
}
public static double microdegreesToDegrees(int microdegrees) {
return (double) microdegrees / (double) MICRODEGREES_COEFF;
}
public void setEndLat(double endLat) {
this.endLat = endLat;
}
public void setEndLng(double endLng) {
this.endLng = endLng;
}
}
view raw BlueLine.java hosted with ❤ by GitHub

The Cloudmade API key goes into the manifest file within the application element like here
<application android:name=".App"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:debuggable="true"
android:theme="@android:style/Theme.NoTitleBar">
<meta-data android:name="CLOUDMADE_KEY" android:value="API key obtained from Cloudmade"/>
.
.
.
</application>
view raw gistfile1.xml hosted with ❤ by GitHub

And that's pretty much it. It's very easy to make it work now with the application:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
.
.
.
BlueLine blueLine = new BlueLine(Color.BLUE, this);
}
Location lastKnownLocation;
//this activity implements LocationListener and has registered for location updates with the system
@Override
public void onLocationChanged(Location loc) {
lastKnownLocation = loc; //save latest location
}
protected void showDirectionsTo(double endLat, double endLng) {
//the routing will be requested from another thread, need to make coordinates accessible from there
blueLine.setEndLat(endLat);
blueLine.setEndLng(endLng);
new Handler().post(new Runnable() {
public void run() {
try {
//will always request directions from current location, destination was assigned previously
blueLine.showDirectionsToPredefinedLocation(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude(), false, "en", true);
mapView.getOverlays().add(blueLine);
}
catch(Exception e) {
e.printStackTrace();
}
}
});
}
view raw gistfile1.java hosted with ❤ by GitHub



That's really it! Hope it helps.

Thursday, May 5, 2011

Offline Google Maps for Android

This took me a while to figure out as I couldn't find any tutorial, but it's fairly easy after all.
The map container of my choice is OSMDroid, which is a great (and open source!) replacement for Google Maps container.
Resolve the MapView and set tile provider as follows
org.osmdroid.views.MapView mapView = (org.osmdroid.views.MapView) findViewById(R.id.map_view); //resolve the map view by id given in the layout
mapView.setTileSource(new OnlineTileSourceBase("Google Maps", ResourceProxy.string.unknown, 1, 20, 256, ".png", "http://mt3.google.com/vt/v=w2.97") {
@Override
public String getTileURLString(final MapTile aTile) {
/*
* GOOGLE MAPS URL looks like
* base url const x y zoom
* http://mt3.google.com/vt/v=w2.97&x=74327&y=50500&z=17
*/
return getBaseUrl() + "&x=" + aTile.getX() + "&y=" + aTile.getY() + "&z=" + aTile.getZoomLevel();
}
});
mapView.setUseDataConnection(false); //this actually makes the controller use only offline tiles
view raw gistfile1.java hosted with ❤ by GitHub

It basically defines a new OnlineTileSourceBase with:
  • name "Google Maps" (this is an important bit, as it will be used to lookup the directory with offline tiles)
  • resource id "unknown" (I also downloaded OSMDroid source code, added a value "google" to the ResourceProxy.string enum and used that instead)
  • minimum zoom level 1
  • maximum zoom level 20
  • tile size 256 pixels
  • tile file extension ".png"
  • tile base url
  • finally inside the overriden method getTileURLString it describes how to build a URL to get a tile for specific location and zoom level
OK, now the controller supports Google Maps and if setUseDataConnection was set to true it would already show everything and would work fine with Google Maps. But the mission is to make it work offline.
The best tool to export the tiles for an area is Mobile Atlas Creator (version up to 1.8, they removed Google Maps from 1.9). Export an area with map source set to Google Maps in desired zoom levels in Osmdroid ZIP format (it's going to take a while). Put the output ZIP file into the /sdcard/osmdroid/Google Maps directory (if it doesn't exist, create it; the name has to be the same as the first parameter in the OnlineTileSourceBase constructor). Again, as I downloaded the source code for OSMDroid, I changed some values in the org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants class (such as OSMDROID_PATH to put the tiles in my directory instead of /sdcard/osmdroid).
Start up the application, now it should show offline tiles. Notice that now, if the SD card is unmounted, the controller will appear empty.
But the story doesn't end here. As the Google Maps terms of use (10.1.3.b) state:
No Pre-Fetching, Caching, or Storage of Content. You must not pre-fetch, cache, or store any Content, except that you may store: (i) limited amounts of Content for the purpose of improving the performance of your Maps API Implementation if you do so temporarily, securely, and in a manner that does not permit use of the Content outside of the Service; and (ii) any content identifier or key that the Maps APIs Documentation specifically permits you to store. For example, you must not use the Content to create an independent database of “places.”
So it sounds like an application is quite limited to use offline Google Maps tiles. 
Nonetheless, it seems they don't disallow to temporarily cache the tiles and work in online mode (of course then one also needs to store the tiles securely). To achieve this, unzip the ZIP file with extracted map area into the location /sdcard/osmdroid/Google Maps/tiles (or whatever is the location specified in OpenStreetMapTileProviderConstants.OSMDROID_PATH), then set mapView.setUseDataConnection(true). The default cache expiry period is not very long, so I also altered it in the source code by setting the values of OpenStreetMapTileProviderConstants.TILE_EXPIRY_TIME_MILLISECONDS and OpenStreetMapTileProviderConstants.DEFAULT_MAXIMUM_CACHED_FILE_AGE to (1000 * 60 * 60 * 24 * 365 * 10) (that's 10 years) . This will make OSMDroid to use pre-fetched tiles for areas where available, but for the rest of the world it will download new tiles.
That's it. Hope it helps.
UPDATE As stated in the comments, you need the Mobile Atlas Creator version 1.8 (I see they removed all versions prior to 1.9 from sourceforge). The other tool capable of fetching tiles is "OsmAnd Map Creator" (I see they deprecated it too, still it's available for download), but I'm not sure what the output directories look like, so one would have to adjust it manually to the structure OSMDroid controller expects.

Friday, April 29, 2011

Android universal Intent to start a "navigation activity"

The mission is what the title says. It was quite hard to define the problem to search for a solution. I ended up asking for help at StackOverflow and as I was suggested, I implemented a way to list some known apps and let the user select a preferred nav app.

So, let's say, somewhere on Activity there's a button, which on click is supposed to call the Navigation application. The way I did it, on tap, the activity shows a custom dialog letting the user select one of the known applications or prompt Android to find suitable activities implicitly.

Here's the source code of the payload of the main method invoked from the OnClickListener - showNavigation():
final int defaultFlag = PackageManager.MATCH_DEFAULT_ONLY;
Intent[] explicitIntents;
//see an example here http://stackoverflow.com/questions/2662531/launching-google-maps-directions-via-an-intent-on-android
private Intent[] getExplicitIntents() {
if(explicitIntents == null) {
PackageManager currentPM = getPackageManager();
explicitIntents = new Intent[]{
new Intent("android.intent.action.navigon.START_PUBLIC"), //navigon with public intent
currentPM.getLaunchIntentForPackage("com.navigon.navigator"), //navigon without public intent
currentPM.getLaunchIntentForPackage("hr.mireo.dp"), //ginius driver dont panic
currentPM.getLaunchIntentForPackage("com.ndrive.android"), //ndrive
currentPM.getLaunchIntentForPackage("com.sygic.aura"), //aura
currentPM.getLaunchIntentForPackage("org.microemu.android.se.appello.lp.Lightpilot") // wisepilot
};
}
return explicitIntents;
}
/**
* Following the suggestion at http://stackoverflow.com/questions/5801684/intent-to-start-a-navigation-activity/5809637#5809637
* Will query up the system for desired applications and then let the user decide which one they would like to launch.
*/
protected void showNavigation() {
//explicit activities (known apps)
//build the list and show it in a dialog
int titleColor = Color.rgb(28, 97, 211);
int headerColor = Color.rgb(98, 151, 251);
HashMap<String, ArrayList<TableRow>> rows = new HashMap<String, ArrayList<TableRow>>();
ArrayList<TableRow> tableRows = new ArrayList<TableRow>();
TableRow row = new TableRow(this);
row.setLayoutParams(new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
row.setGravity(Gravity.CENTER);
row.setBackgroundColor(headerColor);
TextView recyclableTextView = new TextView(this);
recyclableTextView.setText("Explicitly known");
recyclableTextView.setTextColor(Color.BLACK);
recyclableTextView.setTextSize(20);
int screenWidth = getResources().getDisplayMetrics().widthPixels;
recyclableTextView.setWidth(100 * screenWidth / 100); //take up 100% of the width
recyclableTextView.setPadding(5, 5, 5, 5);
row.addView(recyclableTextView);
tableRows.add(row);
PackageManager currentPM = getPackageManager();
for(int i = 0; i < getExplicitIntents().length; i++) {
Intent navigationAppIntent = explicitIntents[i];
try {
for(ResolveInfo explicitActivityInfo : currentPM.queryIntentActivities(navigationAppIntent, defaultFlag)) {
try {
row = new TableRow(this);
row.setLayoutParams(new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
row.setGravity(Gravity.CENTER);
recyclableTextView = new TextView(this);
recyclableTextView.setText(explicitActivityInfo.loadLabel(currentPM) + " (" + explicitActivityInfo.activityInfo.applicationInfo.packageName + ")");
recyclableTextView.setTypeface(Typeface.DEFAULT_BOLD);
recyclableTextView.setTextColor(Color.BLACK);
recyclableTextView.setTextSize(20);
recyclableTextView.setWidth(100 * screenWidth / 100);
recyclableTextView.setPadding(5, 5, 5, 5);
row.addView(recyclableTextView);
row.setContentDescription("" + i); //this descriptor will be used in the following listener to find out which row was selected
//set onclick listener to each row (it already has a descriptor pointing the intent index it represents from the array)
row.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String desc = "" + v.getContentDescription();
if(desc != null) {
try {
startActivity(getExplicitIntents()[Integer.parseInt(desc)]);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
});
row.setMinimumHeight(100);
tableRows.add(row);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
catch(Exception e) {
e.printStackTrace();
}
}
//add implicit apps option
row = new TableRow(this);
row.setLayoutParams(new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
row.setGravity(Gravity.CENTER);
row.addView(makePoiTableRowWithText("Find Implicitly", true, 100));
row.setMinimumHeight(150);
//add a listener to this view to let the OS find matching applications
row.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent implicitIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q="); //NOTE: google navigation can't be launched without destination
startActivity(implicitIntent);
}
});
tableRows.add(row);
rows.put("Compatible apps", tableRows);
showTableDialog(rows, "Launch GPS Navigator", titleColor, R.drawable.menu_gps);
}
view raw gistfile1.java hosted with ❤ by GitHub


Here are few utils needed only to follow up the last call in the showNavigation() method - showTableDialog():
static final int TABLE_DIALOG = 0;
ArrayList<TableRow> rows;
String tableDialogTime;
int tableDialogTableHeaderColor;
int tableDialogTableHeaderIcon;
Dialog dialog;
/**
* Pops up a dialog to show a table.
* @param rows
*/
public void showTableDialog(ArrayList<TableRow> rows, String time, int headerColor, int headerIcon) {
this.rows = rows;
this.tableDialogTableHeaderColor = headerColor;
this.tableDialogTableHeaderIcon = headerIcon;
activityHandler.post(new Runnable() {
@Override
public void run() {
showDialog(TABLE_DIALOG);
}
});
}
public Dialog onCreateDialog(int id) {
switch(id) {
case TABLE_DIALOG:
dialog = new Dialog(this, R.style.TableDialog);
dialog.setContentView(R.layout.table_dialog);
break;
default:
dialog = null;
}
return dialog;
}
public void onPrepareDialog(int id, Dialog dialog) {
if(id == TABLE_DIALOG) {
//set icon
((ImageView) dialog.findViewById(R.id.table_dialog_icon)).setBackgroundDrawable(getResources().getDrawable(tableDialogTableHeaderIcon));
//set background for the whole header
dialog.findViewById(R.id.table_dialog_header).setBackgroundColor(tableDialogTableHeaderColor);
//set title
TextView titleField = (TextView) dialog.findViewById(R.id.table_dialog_title);
titleField.setText(title);
//set up the header for the table on a separate TableLayout (it's a separate TableLayout, so that it doesn't scroll along with the rest of the scrollable area)
TableLayout tableHeader = (TableLayout) dialog.findViewById(R.id.table_header);
tableHeader.removeAllViews();
tableHeader.addView(rows.get(0));
//set up the table
TableLayout table = (TableLayout) dialog.findViewById(R.id.table_dialog_layout);
//clean up old entries (on re-invoke)
table.removeAllViews();
//populate table rows: add rows with separator inbetween
for(int i = 1; i < rows.size(); i++) {
int colorAmount = i % 2 == 0? 230 : 200;
TableRow row = rows.get(i);
row.setBackgroundColor(Color.rgb(colorAmount, colorAmount, colorAmount));
table.addView(row);
//add a separator after each line
View separator = new View(dialog.getContext());
separator.setBackgroundColor(Color.rgb(77, 77, 77));
separator.setMinimumHeight(2);
table.addView(separator);
}
}
}
private TextView recyclableTextView;
public TextView makePoiTableRowWithText(CharSequence text, boolean bold, int widthInPercentOfScreenWidth) {
recyclableTextView = new TextView(this);
recyclableTextView.setText(text);
recyclableTextView.setTypeface(bold? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
recyclableTextView.setTextColor(Color.BLACK);
recyclableTextView.setTextSize(20);
recyclableTextView.setWidth(widthInPercentOfScreenWidth * screenWidth / 100);
recyclableTextView.setPadding(5, 5, 5, 5);
return recyclableTextView;
}
view raw gistfile1.java hosted with ❤ by GitHub


These are the resource files:
* this goes to /res/values/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TableDialog" parent="android:style/Theme.Dialog">
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
</style>
</resources>
view raw themes.xml hosted with ❤ by GitHub

* this goes to /res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DialogText">
<item name="android:textColor">#FF231f20</item>
<item name="android:textSize">25sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="DialogText.Title">
<item name="android:textColor">#FF364945</item>
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
</style>
</resources>
view raw styles.xml hosted with ❤ by GitHub

* this goes to /res/layout/table_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:gravity="center_horizontal"
android:id="@+id/table_dialog_header">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/table_dialog_icon"/>
<TextView
android:id="@+id/table_dialog_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:textSize="30sp"
android:textColor="#fff"
android:textStyle="bold"/>
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_horizontal"
android:id="@+id/fillable_area">
<TableLayout
android:id="@+id/table_header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<ScrollView
android:id="@+id/scrollable_table"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TableLayout
android:id="@+id/table_dialog_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</ScrollView>
</LinearLayout>
</LinearLayout>

* this goes to /res/drawable as menu_gps.png







And here is the result dialog you are supposed to see:

On click on each row it will start up the respective activity.
That's it, I hope it helps!