Graphing Data
So, as of this writing, there doesn't seem to be a good Java graphing API made for the Android 2D & 3d graphics libraries. Most of the stuff out there is based off Swing and AWT which is not supported in Android.
That left us a few options. One, we could try to refactor an existing library and replace the drawing code with Android calls, since coordinates are largely the same, we just needed to update the API calls. That might have taken a bit too long.
There are a few Android projects that have graphing components, but their initial implementations seemed a bit quirky to learn and use.
The other option was to consider using Google Charts and do URL calls with the data we want. That would offload the graphics part, but that presented a problem for our clients as the eventual usage of this was with offline disconnected places with poor connectivity.
Initially, I dismissed some of the help on stackoverflow as completely unhelpful when they suggested building it in HTML. But suddenly it dawned on me that the browser built into Android was surprisingly powerful. It could use a lot of the Javascript frameworks out there like jQuery and Dojo.
The Goal
- Let's use a Javascript based graphing package that can render graph in a browser.
- We want this to be done locally with just the Javascript and HTML and data we have on hand.
- No connections to the internet
- It should be an Activity in android that you can seamlessly jump to and from.
- In other words, it should be embedded and look awesome.
Enter Flot
http://code.google.com/p/flot/
After some tinkering around, I found this jQuery based graphing package called Flot. It's incredibly straightforward to use. Correction, it is pretty awesome. You define your data in Javascript via a JSON like object and pass it into the library. It'll automagically draw itself on screen within the boundaries you set autoscaling the axes to fit.
We confirmed that this was renderable and interactive with most of the functionality it offered in the Android Webkit browser. The question now was, how to get it ready to receive data in our program?
Local HTML and Javascript graphing package, check.
Enter Webkit.Webview
In Android, there's a View for your activity called the WebView. It's basically the WebView browser accessible as an item in your Activity view. You can make it take up the entire screen, or move it around or whatever.
In eclipse it's oddly not visible in the dropdown of Views to add to your layout. But here is the syntax I just cut and pasted into to my view xml:
<WebView android:id="@+id/wv1" android:layout_height="wrap_content" android:layout_width="fill_parent" />
You'll need to access it in your Activity in order to get it ready to run in your app.
public class MyActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); WebView wv = (WebView) findViewById(R.id.wv1);
Ok, let's stop there. We've now got a handle to the WebView. Let's now load up our HTML page into the webview.
Learning WebView's Quirks
So, it's not so simple in Android to just have your WebView (or any other application) just willy nilly request a file from the filesystem. This apparently is a security precaution in android. You need to really know what you want and where to get your file assets. Normally you'd do this by making a ContentProvider with filehandles and such for media and other things. But arbitrary file access is generally not allowed.
What you can do however is get easy access to stuff in your app in your assets/ folder. There are two methods to grab these things.
First is by getting an inputstream:
try { InputStream is = getAssets().open("file.html"); int size = is.available(); // Read the entire asset into a local byte buffer. byte[] buffer = new byte[size]; is.read(buffer); is.close(); // Convert the buffer into a Java string. String text = new String(buffer); // Load the local file into the webview appView.loadData(text, "text/html", "UTF-8"); } catch (IOException e) { // Should never happen! throw new RuntimeException(e); }
Or, in the method that I seem to prefer, is the reserved file:///android_asset/ URI definition. This defines the URI at the root of your assets folder. So if you have a file in mydir/myfile.html, you can access it via file://android_asset/mydir/myfile.html
So, let's finish up dealing with the WebView in the onCreate method:
WebView wv = (WebView) findViewById(R.id.wv1); MyHandlerClass myhandler = new MyHandlerClass (wv); wv.getSettings().setJavaScriptEnabled(true); wv.addJavascriptInterface(myhandler, "testhandler"); wv.loadUrl("file:///android_asset/flot/html/dynamic.html");
There are a few important things here.
- I'm enabling the JavaScript engine to run in the WebView? I'm handling
- I'm adding a JavaScript interface. This allows me to have our HTML file get access back to our Java class. More on that in the next section.
- loadUrl actually loads up the beginning HTML file to display in our Activity.
Java and Javascript Interaction
So let's look at the addJavaScriptInterface call. What this does is lets me define a class to receive calls defined by the string testhandler.
Let's see how we call it in the actual HTML page:
<script id="source" language="javascript" type="text/javascript"> function load(){ document.getElementById('graphtitle').innerHTML = window.testhandler.getGraphTitle(); window.testhandler.loadGraph(); } function GotGraph(gdata) { $.plot($("#placeholder"), gdata); } </script>
For reference, I have two things going on here. One, the getGraphTitle() call tries to reset an H1 heading for display purposes. The $.plot() call is a Flot call for rendering the graph.
Let's take a look at the MyHandlerClass? class:
public class MyHandlerClass { private WebView mAppView; public MyHandlerClass (WebView appView) { this.mAppView = appView; } public String getGraphTitle() { return "This is my graph, baby!"; } public void loadGraph() { JSONArray arr = new JSONArray(); JSONObject result = new JSONObject(); try { result.put("data", getRawDataJSON()); //will ultimately look like: {"data": p[x1,y1],[x2,y2],[x3,y3],[]....]}, result.put("lines", getLineOptionsJSON()); //{ "lines": { "show" : true }}, result.put("points", getFalseJSON()); // { "points": { "show" : true }} arr.put(result); } catch (Exception ex) { //do something } //return arr.toString(); //This _WILL_ return the data in a good looking JSON string, but if you pass it straight into the Flot Plot method, it will not work! mAppView.loadUrl("javascript:GotGraph(" + arr.toString() + ")"); //this callback works! }
Notice for getGraphTitle(), we're just returning a string. This works fine. And returning a JSON string for evaluation and interpretation usually works. You can eval() and do all sorts of good stuff with the JavaScript in a WebView.
But for the longest time, I could not get loadGraph() to work by returning a JSON string to render the graph on the load() function (which is run onload()). For some reason the rendering order or something prevented the JSON data I was generating to actually return and be interpreted by the graphing engine.
That's where the local instance of mAppView comes in. With the handle to the WebView? you're using, you can from Java do a callback via an alternate JavaScript method to push data or trigger events. In this case, I'm doing a callback to a separate method called GotGraph?(). The WebView? gives you the ability to trigger JavaScript method by calling loadUrl on the same view with the javascript prefix. Having done that, the graph will update itself as per what you put in your method.
This may become important as you have menus and other Android controls doing things around the WebView?. You will be able to have the Android events trigger events within the WebView?'s control and vice versa. You can also send from JavaScript to Javaland via the JavaScript method you expose. For example, you can add parameters to getGraphTitle() to include string or int args for processing.