Introduction
Our Android app has a new feature using the camera to take photos. We can split the feature into two tasks, taking a photo and reviewing a photo by pan-and-zoom.
Take a Photo
To take a photo, we found two methods. The first method uses an Intent
to launch the camera app. The second method uses the Camera API to access the hardware directly. For simplicity in our project, we chose the Intent
method. Using the Intent
to call the camera reduces the amount of time needed to develop taking a photo. By using the Intent
approach, we can leverage all the built-in functionality provided by the OS layer.
To start we need to set permission to get access to the camera in the AnroidManifest.xml
1
2
3
|
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
The android:required="true"
lets the Google Play Store detect if the app should be installed or not, this is optional. If the attribute android:required="true"
is omitted, then the app has to check for the feature at runtime. Otherwise, the app will crash when it attempts to access the camera when one does not exist:
1
2
3
4
|
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) && Camera.getNumberOfCameras() > 0) {
// this device has a camera
...
}
|
The first part of checks that the device has a camera, which may return true even though there is none, and the next part checks for the number of cameras the device has.
Using Intent
to call an existing camera app:
1
2
3
4
5
6
7
|
private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
File file = new File(...);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
startActivityForResult(cameraIntent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
|
The CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE
can be any number.
The return from the Intent
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
if (data != null && resultCode == RESULT_OK) {
// Image captured and saved to file
...
} else {
// User cancelled the image capture or
// image capture failed, advise user
...
}
}
super.onActivityResult(requestCode, resultCode, data);
}
|
By checking if the data is null 1 prevents the following error when the user press the back button or cancels:
1 |
java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=100, result=0, data=null} to activity … java.lang.NullPointerException
|
Pan and Zoom to Review a Photo
To review a photo afterward, we came across several issues to overcome. These issues included out of memory and image pan-and-zoom.
Memory
Memory issues encountered using ImageView
were the limited amount of RAM in a phone, i.e. Nexus S with 384 MB of available RAM out of 512 MB2.
The following setup prevents an OutOfMemory exception.
1
2
3
4
|
BitmapFactory.Options o = new BitmapFactory.Options();
o.inPurgeable = true;
o.inInputShareable = true;
o.inJustDecodeBounds = false;
|
To prevent running out of memory with Nexus S, use a byte array to store the image and use BitmapFactory.decodeByteArray
to store the photo as a Bitmap
. This is recommended over reading the file directly into a Bitmap
3.
The file provided to BitmapFactory.decodeByteArray
is read using Google Guava4 ByteStreams. Other approaches are documented on Stack Overflow5,6.
1
2
3
4
5
6
7
8
9
10
11
|
FileInputStream fInStream = new FileInputStream(...);
...
byte byteArray[] = null;
try {
if(fInStream != null)
byteArray = ByteStreams.toByteArray(fInStream);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
|
Once the byte array is loaded, it can be stored as a Bitmap
for ImageView
.
1 |
Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length, o);
|
The bitmap also needs to be scale down before sending it to ImageView
to prevent the following error message:
1 |
Bitmap too large to be uploaded into a texture (..., max=2048x2048) |
While the original bitmap is still in memory, this is solved by following the solution mentioned in Stack Overflow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
ImageView mImageView = (ImageView) findViewById(R.id.imageView);
...
int maxSize = 2048;
int height = 0;
int width = 0;
int inHeight = bitmap.getHeight();
int inWidth = bitmap.getWidth();
if(inWidth > inHeight) { // photo is landscape
height = (inHeight * maxSize) / inWidth;
width = maxSize;
} else { // photo is portrait
height = maxSize;
width = (inWidth * maxSize) / inHeight;
}
Bitmap bitmapResized = Bitmap.createScaledBitmap(bitmap, width, height, true);
if(bitmapResized != null) {
// call for GC on previous Bitmap using Bitmap recycle()<sup><a href="#7">7</a></sup>
...
mImageView.setImageBitmap(bitmapResized);
mImageView.invalidate();
...
}
|
Zoom
ImageView
does not provide the ability to zoom into a image. One option is to use ZoomControls
widget and listen to OnTouch
events. Another option is to show the image in a WebView
. A third option is to use Michael Ortiz TouchImageView 8. We choose TouchImageView
because it offered an easy way to get zoom functionality without too much extra coding.
To use TouchImageView
in the layout xml do, the following:
1
2
3
4
|
<com.example.touch.TouchImageView
android:id="@+id/touchImageView”
android:layout_width="match_parent"
android:layout_height="match_parent" />
|
Without setting an image in the layout xml but programmatically as discussed above, the following error will show:
1 |
onMeasure() did not set the measured dimension by calling setMeasuredDimension() |
After investigating why this happens, we found that onMeasure(...)
in TouchImageView
has the following code:
1
2
3
4
5
6
7
8
9
|
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Drawable drawable = getDrawable();
if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
setMeasuredDimension(0, 0);
return;
}
...
}
|
When getDrawable()
is called it returns null
. The conditional check causes the app to crash.
The solution was to include an image source in the layout xml. A blank PNG image did the trick.
1
2
3
4
5
|
<com.example.touch.TouchImageView
android:id="@+id/touchImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="..." />
|
The ability to swap out ImageView
with TouchImageView
in code is an advantage. There is no visible or noticeable lag when zooming in and out.
1 java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=1888, result=0, data=null} to activity
3 BitmapFactory.decodeFile out of memory with images 2400×2400
5 Elegant way to read file into byte[] array in Java [duplicate]
7 Is it needed to call Bitmap.recycle() after used (in Android)?
8 How can I get zoom functionality for images?
Why would I ever NOT use BitmapFactory’s inPurgeable option?