The UI for the application is modest. The things the UI must do include responding to two different buttons, one for taking a picture and one for calling the JNI functions.
Beyond that, the code performs simple operations to display the various bitmap images. Let’s start by looking at the layout for this application.
19.4.1 User interface layout
The layout for this application is contained in the resource file main.xml, shown next.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ffffffff">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#ffffffff"
android:gravity="center">
<Button android:id="@+id/AcquireImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Acquire Image"
/>
<Button android:id="@+id/FindEdges"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Find Edges"
/>
Listing 19.7 Layout file Figure 19.7 Building the JNI library
Outer layout
b
Layout containing Buttons
c
Acquire, Find Edges Buttons
d
</LinearLayout>
<ImageView android:id="@+id/PictureFrame"
android:layout_width="320px"
android:layout_height="240px"
android:scaleType="centerCrop"
android:layout_gravity="center_vertical|center_horizontal"/>
</LinearLayout>
This layout is straightforward. It contains a vertically oriented LinearLayoutB, which
contains all the UI elements of this application. Next is a horizontally oriented Layout, which is also centered c. This layout contains two Buttons d, one for the acquisition of a photo and one for calling the image-processing routines. When an image is avail- able, it’s shown in an ImageView instance e. The visibility of the FindEdgesButton is toggled on only after a photo is available.
This application relies on an Application object to hold a global variable—in this case, a Bitmap. This is necessary because a photo application often results in the user changing the orientation of the device: portrait to landscape or landscape to portrait, and so on. Whenever this occurs, Android’s default behavior is to restart the Activ- ity. If you store a captured photo in an Activity-level variable, you’ll lose it each time the device is rotated. To solve this problem, store the Bitmap in an Application object. The following listing shows the code for this simple class.
package com.msi.manning.ua2efindedges;
import android.app.Application;
import android.graphics.Bitmap;
public class UA2EFindEdgesApp extends Application { private Bitmap b;
public Bitmap getBitmap() { return b;
}
public void setBitmap(Bitmap b) { this.b = b;
} }
The Application and Bitmap classes must be imported B for this code to compile. The UA2EFindEdgesApp class extends the Application class. The Bitmap is stored as a pri- vate member, and of course we have a getter and setter c to manipulate this Bitmap.
NOTE Whenever you use an Application class, it must be defined in the AndroidManifest.xml file as the android:name attribute of the application tag.
Let’s now look at the primary user interface code to see how you take a photo and store it into the Application object.
Listing 19.8 UA2EFindEdgesApp.java
ImageView
e
Required imports
b
Getter and setter routines
c
19.4.2 Taking a photo
There are a number of ways to take a photograph on the Android platform. For this application, you’ll just ask the Camera to do the work for you through the use of an Intent. The next listing demonstrates this approach. Note that the JNI-related code introduced next is employed in listing 19.10.
package com.msi.manning.ua2efindedges;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Button;
import android.view.View;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
public class UA2EFindEdges extends Activity { protected ImageView imageView = null;
private final String tag = "UA2EFindEdges";
private Button btnAcquire;
private Button btnFindEdges;
// declare native methods
public native int converttogray(Bitmap bitmapcolor, Bitmap gray);
public native int detectedges(Bitmap bitmapgray, Bitmap bitmapedges);
static {
System.loadLibrary("ua2efindedges");
}
@Override
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnAcquire = (Button) this.findViewById(R.id.AcquireImage);
btnAcquire.setOnClickListener(new View.OnClickListener(){
public void onClick(View v){
try {
Intent action = new
Intent("android.media.action.IMAGE_CAPTURE");
startActivityForResult(action,1);
} catch (Exception e) {
Log.e(tag,"Error occurred [" + e.getMessage() + "]");
} } });
btnFindEdges = (Button) this.findViewById(R.id.FindEdges);
btnFindEdges.setOnClickListener(new View.OnClickListener(){
// code shown in next listing Listing 19.9 UA2EFindEdges.java
Declare native methods
b
Load JNI library
c
Request photo
d
});
imageView = (ImageView) this.findViewById(R.id.PictureFrame);
UA2EFindEdgesApp app = (UA2EFindEdgesApp) getApplication();
Bitmap b = app.getBitmap();
if (b != null) {
imageView.setImageBitmap(b);
} else {
btnFindEdges.setVisibility(View.GONE);
} }
protected void onActivityResult(int requestCode, int resultCode,Intent data)
{ try {
if (requestCode == 1) {
if (resultCode == RESULT_OK) {
UA2EFindEdgesApp app = (UA2EFindEdgesApp) getApplication();
Bitmap b = app.getBitmap();
if (b != null) { b.recycle();
}
b = (Bitmap) data.getExtras().get("data");
app.setBitmap(b);
if (b != null) {
imageView.setImageBitmap(b);
btnFindEdges.setVisibility(View.VISIBLE);
} } }
} catch (Exception e) {
Log.e(tag,"onActivityResult Error [" + e.getMessage() + "]");
} } }
This code is the primary Activity for the application and is also the code that calls the previously written native code. To call the native methods, they must be declared B
and the JNI library must be loaded c at runtime. When the Acquire button is selected, we request a photo by creating an Intent and dispatching it with a call to startActivityForResult d. Whenever the Activity is created, we need to check whether a Bitmap is available; to do that we first get a reference to the Application object e, casting it to the UA2EFindEdgesApp class. If we have a valid Bitmap, we display
it f in the ImageView. This approach handles the scenario where the Android device
has changed orientations and the Activity has been restarted. When the Camera has captured a photo, it’s packaged up into an Intent and sent back to the application via the onActivityResult() method g. If an existing Bitmap is found in the Applica- tion object, it’s recycled h and the new one is extracted from the Intent i. The Bitmap is stored in the Application object and displayed to the screen. Once the photo has been acquired, the Find Edges button is shown J.
e
Get Application reference Display
Bitmap
f
Activity result
g
Recycle Bitmap
h
Extract, store, display Bitmap
i
Toggle Button visibility
j
Now that you have the photo, you want to run your image-processing routines against it to find the edges. That’s up next as we examine the click handler for the Find Edges button.
19.4.3 Finding the edges
The Java side of the image-processing code relies on some knowledge of how Android Bitmaps are constructed. There are three Bitmaps in play by the time the edge detec- tion is complete:
The original photo is stored in the Application object as a full-color Bitmap with a format of AARRGGBB, meaning Alpha channel, Red, Green, and Blue pixel data.
A grayscale image is created from the color image.
A second grayscale image is created, which receives the edges found in the prior grayscale image.
The following listing shows the click handler and steps through the process of con- verting a color image to an edges-only image.
btnFindEdges = (Button) this.findViewById(R.id.ModifyImage);
btnFindEdges.setOnClickListener(new View.OnClickListener(){
public void onClick(View v){
try {
UA2EFindEdgesApp app = (UA2EFindEdgesApp) getApplication();
Bitmap b = app.getBitmap();
int width = b.getWidth();
int height = b.getHeight();
Bitmap bg = Bitmap.createBitmap(width, height, Config.ALPHA_8);
Bitmap be = Bitmap.createBitmap(width, height, Config.ALPHA_8);
converttogray(b,bg);
detectedges(bg,be);
app.setBitmap(be);
imageView.setImageBitmap(be);
btnFindEdges.setVisibility(View.GONE);
} catch (Exception e) {
Log.e(tag,"Error occured [" + e.getMessage() + "]");
} } });
This code first obtains a reference to the Application object to retrieve the color BitmapB. To create the two grayscale Bitmaps c required for the image processing, we first need to get the dimensions of the original photo. With all of the Bitmaps ready for use, we convert the color photo to a grayscale version with a call to con- verttogray(). Next, find the edges in the image with a call to detectedges() d. Store the new image in the application and display it on the screen. We don’t want to run the edge-detection routine without first obtaining a new color image, so we hide the Find Edges button.
Listing 19.10 Finding the edges
b
Get Bitmap from Application
c
Create two grayscale Bitmaps Find edges
d
Congratulations—you now know how to use the NDK! All in all, it wasn’t painful, but there’s one annoying task. Building the JNI library from a command line is simple but not fun, particularly when you’re accustomed to saving a Java source file and hav- ing the application auto-build. Fortunately, there’s a way to incorporate the NDK into the Eclipse environment.