Taking Pictures - Android Application Development: A Beginner's Tutorial (2015)

Android Application Development: A Beginner's Tutorial (2015)

Chapter 19. Taking Pictures

Almost all Android handsets and tablets come with one or two cameras. You can use a camera to take still pictures by starting an activity in the built-in Camera application or use the Camera API.

This chapter shows how to use both approaches.

Overview

An Android application can call another application to use one or two features offered by the latter. For example, to send an email from your application, you can use the default Email application rather than writing your own app. In the case of taking a picture, the easiest way to do this is by using the Camera application. To activate Camera, use the following code.

int requestCode = ...;

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

startActivityForResult(intent, requestCode);

Basically, you need to create an Intent by passing MediaStore.ACTION_IMAGE_CAPTURE to the Intent class’s constructor. Then, you need to call startActivityForResult from your activity passing the Intent and a request code. The request code can be any integer your heart desires. You will learn shortly the purpose of passing a request code.

To tell Camera where to save the taken picture, you can pass a Uri to the Intent. Here is the complete code.

int requestCode = ...;

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

Uri uri = ...;

intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);

startActivityForResult(intent, requestCode);

When the user closes Camera after taking a picture or canceling the operation, Android will notify your application by calling the onActivityResult method in the activity that called Camera. This gives you the opportunity to save the picture taken using Camera. The signature ofonActivityResult is as follows.

protected void onActivityResult(int requestCode, int resultCode,

android.content.Intent data)

The system calls onActivityResult by passing three arguments. The first argument, requestCode, is the request code passed when calling startActivityForResult. The request code is important if you call other activities from your activity, passing a different request code each time. Since you can only have one onActivityResult implementation in your activity, all calls to startActivityForResult will share the same onActivityResult, and you need to know which activity caused onActivityResult to be called by checking the request code.

The second argument to onActivityResult is a result code. The value can be either Activity.RESULT_OK or Activity.RESULT_CANCELED or a user defined value. Activity.RESULT_OK indicates that the operation succeeded and Activity.RESULT_CANCELED indicates that the operation was canceled.

The third argument to onActivityResult contains data from the called activity if the operation was successful.

Using Camera is easy. However, if Camera does not suit your needs, you can also use the Camera API directly. This is not as easy as using Camera, but the API lets you configure many aspects of the camera.

The samples accompanying this chapter show you both methods.

Using Camera

To use the camera, you need these in your manifest.

<uses-feature android:name="android.hardware.camera"/>

<uses-permission android:name="android.permission.CAMERA"/>

The CameraDemo application shows how to use the built-in intent to activate the Camera application and use it to take a picture. CameraDemo has only one activity, which sports two button on its action bar, Show Camera and Email. The Show Camera button starts Camera and the Email button emails the picture. The application is shown in Figure 19.1.

image

Figure 19.1: CameraDemo

Let’s start dissecting the code, starting from the manifest.

Listing 19.1: The manifest

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.example.camerademo" >

<uses-feature android:name="android.hardware.camera"/>

<uses-permission android:name="android.permission.CAMERA"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application

android:allowBackup="true"

android:icon="@drawable/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:name="com.example.camerademo.MainActivity"

android:label="@string/app_name" >

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

menu file (the main.xml file in Listing 19.1) that contains two menu items for the action bar.

Listing 19.1: The menu file (menu_main.xml)

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

<item

android:id="@+id/action_camera"

android:orderInCategory="100"

android:showAsAction="ifRoom"

android:title="@string/action_show_camera"/>

<item

android:id="@+id/action_email"

android:orderInCategory="200"

android:showAsAction="ifRoom"

android:title="@string/action_email"/>

</menu>

The layout file for the main activity is presented in Listing 19.2. It contains an ImageView for showing the taken picture. The activity class itself is shown in Listing 19.3.

Listing 19.2: The activity_main.xml file

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:paddingBottom="@dimen/activity_vertical_margin"

android:paddingLeft="@dimen/activity_horizontal_margin"

android:paddingRight="@dimen/activity_horizontal_margin"

android:paddingTop="@dimen/activity_vertical_margin">

<ImageView

android:id="@+id/imageView"

android:layout_width="match_parent"

android:layout_height="match_parent"

/>

</RelativeLayout>

Listing 19.3: The MainActivity class

package com.example.camerademo;

import java.io.File;

import android.app.Activity;

import android.content.Intent;

import android.net.Uri;

import android.os.Bundle;

import android.os.Environment;

import android.provider.MediaStore;

import android.util.Log;

import android.view.Menu;

import android.view.MenuItem;

import android.widget.ImageView;

import android.widget.Toast;

public class MainActivity extends Activity {

private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;

File pictureDir = new File(Environment.getExternalStoragePublicDirectory(

Environment.DIRECTORY_PICTURES), "CameraDemo");

private static final String FILE_NAME = "image01.jpg";

private Uri fileUri;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

if (!pictureDir.exists()) {

pictureDir.mkdirs();

}

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

getMenuInflater().inflate(R.menu.menu_main, menu);

return true;

}

@Override

public boolean onOptionsItemSelected(MenuItem item) {

switch (item.getItemId()) {

case R.id.action_camera:

showCamera();

return true;

case R.id.action_email:

emailPicture();

return true;

default:

return super.onContextItemSelected(item);

}

}

private void showCamera() {

Intent intent = new Intent(

MediaStore.ACTION_IMAGE_CAPTURE);

File image = new File(pictureDir, FILE_NAME);

fileUri = Uri.fromFile(image);

intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);

// check if the device has a camera:

if (intent.resolveActivity(getPackageManager()) != null) {

startActivityForResult(intent,

CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);

}

}

@Override

protected void onActivityResult(int requestCode,

int resultCode, Intent data) {

if (requestCode ==

CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {

if (resultCode == RESULT_OK) {

ImageView imageView = (ImageView)

findViewById(R.id.imageView);

File image = new File(pictureDir, FILE_NAME);

fileUri = Uri.fromFile(image);

imageView.setImageURI(fileUri);

} else if (resultCode == RESULT_CANCELED) {

Toast.makeText(this, "Action cancelled",

Toast.LENGTH_LONG).show();

} else {

Toast.makeText(this, "Error",

Toast.LENGTH_LONG).show();

}

}

}

private void emailPicture() {

Intent emailIntent = new Intent(

android.content.Intent.ACTION_SEND);

emailIntent.setType("application/image");

emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,

new String[]{"me@example.com"});

emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,

"New photo");

emailIntent.putExtra(android.content.Intent.EXTRA_TEXT,

"From My App");

emailIntent.putExtra(Intent.EXTRA_STREAM, fileUri);

startActivity(Intent.createChooser(emailIntent,

"Send mail..."));

}

}

The Show Camera button in MainActivity calls the showCamera method. This method starts Camera by calling startActivityForResult. The emailPicture method starts another activity that in turn activates the default Email application.

The Camera API

At the center of the Camera API is the android.hardware.Camera class. A Camera represents a digital camera.

Every camera has a viewfinder, through which the photographer can see what the camera is seeing. A viewfinder can be optical or electronic. An analog camera normally offers an optical viewfinder, which is a reversed telescope mounted on the camera body. Some digital cameras have an electronic viewfinder and some have an electronic one plus an optical one. On an Android tablet and handset, the whole screen or part of the screen is normally used as a viewfinder.

In an application that uses a camera, the android.view.SurfaceView class is normally used as a viewfinder. SurfaceView is a subclass of View and, as such, can be added to an activity by declaring it in a layout file using the SurfaceView element. The area of a SurfaceView will be continuously updated with what the camera sees. You control a SurfaceView through its SurfaceHolder, which you can obtain by calling the getHolder method on the SurfaceView. SurfaceHolder is an interface in the android.view package.

Therefore, when working with a camera, you need to manage an instance of Camera as well as a SurfaceHolder.

Managing A Camera

When working with the Camera API, you should start by checking if the device does have a camera. You must also determine which camera to use if a device has multiple cameras. You do it by calling the open static method of the Camera class.

Camera camera = null;

try {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {

camera = Camera.open(0);

} else {

camera = Camera.open();

}

} catch (Exception e) {

e.printStackTrace();

}

For pre-Gingerbread Android (Android version 2.3), use the no-argument method overload. For Android version 2.3, use the overload that takes an integer.

public static Camera open(int cameraId)

Passing 0 to the method gives you the first camera, 1 the second camera, and so on.

You should enclose the call to open in a try block as it may throw an exception.

Once you obtain a Camera, pass a SurfaceHolder to the setPreviewDisplay method on the Camera.

public void setPreviewDisplay(android.view.SurfaceHolder holder)

If setPreviewDisplay returns successfully, call the camera’s startPreview method and the SurfaceView controlled by the SurfaceHolder will start displaying what the camera sees.

To take a picture, call the camera’s takePicture method. After a picture is taken, the preview will stop so you will need to call startPreview again to take another picture.

When you are finished with the camera, call stopPreview and release to release the camera.

Optionally, you can configure the camera after you call open by calling its getParameters method, modifying the parameters, and passing them back to the camera using the setParameters method.

With the takePicture method you can decide what to do to the resulting raw and JPEG images from the camera. The signature of takePicture is as follows.

public final void takePicture(Camera.ShutterCallback shutter,

Camera.PictureCallback raw, Camera.PictureCallback postview,

Camera.PictureCallback jpeg)

The four parameters are these.

§ shutter. The callback for image capture moment. For example, you can pass code that plays a click sound to make it more like a real camera.

§ raw. The callback for uncompressed image data.

§ postview. The callback with postview image data.

§ jpeg. The callback for JPEG image data.

You will learn how to use Camera in the CameraAPIDemo application.

Managing A SurfaceHolder

A SurfaceHolder communicates with its user through a series of methods in SurfaceHolder.Callback. To manage a SurfaceHolder, you need to pass an instance of SurfaceHolder.Callback to the SurfaceHolder’s addCallback method.

SurfaceHolder.Callback exposes these three methods that the SurfaceHolder will call in response to events.

public abstract void surfaceChanged(SurfaceHolder holder,

int format, int width, int height)

Called after any structural changes (format or size) have been made to the surface.

public abstract void surfaceCreated(SurfaceHolder holder)

Called after the surface is first created.

public abstract void surfaceDestroyed(SurfaceHolder holder)

Called before a surface is being destroyed.

For instance, you might want to link a SurfaceHolder with a Camera right after the SurfaceHolder is created. Therefore, you might want to override the surfaceCreated method with this code.

@Override

public void surfaceCreated(SurfaceHolder holder) {

try {

camera.setPreviewDisplay(holder);

camera.startPreview();

} catch (Exception e){

Log.d("camera", e.getMessage());

}

}

Using the Camera API

The CameraAPIDemo application demonstrates the use of the Camera API to take still pictures. It uses a SurfaceView as a viewfinder and a button to take a picture. Clicking the button takes the picture and emits a beep sound. After a picture is taken, the SurfaceView freezes for two seconds to give the user the change to inspect the picture and restart the camera preview to allow the user to take another picture. All pictures are given a random name and stored in the external storage.

The application has one activity, whose layout is shown in Listing 19.4.

Listing 19.4: The layout file (activity_main.xml)

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

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="fill_parent">

<Button

android:id="@+id/button1"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="center"

android:onClick="takePicture"

android:text="@string/button_take"/>

<SurfaceView

android:id="@+id/surfaceview"

android:layout_width="match_parent"

android:layout_height="match_parent" />

</LinearLayout>

The layout features a LinearLayout containing a button and a SurfaceView. The activity class is presented in Listing 19.5.

Listing 19.5: The MainActivity class

package com.example.cameraapidemo;

import java.io.File;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import android.app.Activity;

import android.hardware.Camera;

import android.hardware.Camera.PictureCallback;

import android.hardware.Camera.ShutterCallback;

import android.media.AudioManager;

import android.media.SoundPool;

import android.net.Uri;

import android.os.Build;

import android.os.Bundle;

import android.os.Environment;

import android.os.Handler;

import android.provider.Settings;

import android.util.Log;

import android.view.Menu;

import android.view.SurfaceHolder;

import android.view.SurfaceView;

import android.view.View;

import android.widget.Button;

import android.widget.Toast;

public class MainActivity extends Activity

implements SurfaceHolder.Callback {

private Camera camera;

SoundPool soundPool;

int beepId;

File pictureDir = new File(Environment

.getExternalStoragePublicDirectory(

Environment.DIRECTORY_PICTURES),

"CameraAPIDemo");

private static final String TAG = "camera";

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

pictureDir.mkdirs();

soundPool = new SoundPool(1,

AudioManager.STREAM_NOTIFICATION, 0);

Uri uri = Settings.System.DEFAULT_RINGTONE_URI;

beepId = soundPool.load(uri.getPath(), 1);

SurfaceView surfaceView = (SurfaceView)

findViewById(R.id.surfaceview);

surfaceView.getHolder().addCallback(this);

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

getMenuInflater().inflate(R.menu.menu_main, menu);

return true;

}

@Override

public void onResume() {

super.onResume();

try {

if (Build.VERSION.SDK_INT >=

Build.VERSION_CODES.GINGERBREAD) {

camera = Camera.open(0);

} else {

camera = Camera.open();

}

} catch (Exception e) {

e.printStackTrace();

}

}

@Override

public void onPause() {

super.onPause();

if (camera != null) {

try {

camera.release();

camera = null;

} catch (Exception e) {

e.printStackTrace();

}

}

}

private void enableButton(boolean enabled) {

Button button = (Button) findViewById(R.id.button1);

button.setEnabled(enabled);

}

public void takePicture(View view) {

enableButton(false);

camera.takePicture(shutterCallback, null,

pictureCallback);

}

private ShutterCallback shutterCallback =

new ShutterCallback() {

@Override

public void onShutter() {

// play sound

soundPool.play(beepId, 1.0f, 1.0f, 0, 0, 1.0f);

}

};

private PictureCallback pictureCallback =

new PictureCallback() {

@Override

public void onPictureTaken(byte[] data,

final Camera camera) {

Toast.makeText(MainActivity.this, "Saving image",

Toast.LENGTH_LONG)

.show();

File pictureFile = new File(pictureDir,

System.currentTimeMillis() + ".jpg");

try {

FileOutputStream fos = new FileOutputStream(

pictureFile);

fos.write(data);

fos.close();

} catch (FileNotFoundException e) {

Log.d(TAG, e.getMessage());

} catch (IOException e) {

Log.d(TAG, e.getMessage());

}

Handler handler = new Handler();

handler.postDelayed(new Runnable() {

@Override

public void run() {

try {

enableButton(true);

camera.startPreview();

} catch (Exception e) {

Log.d("camera",

"Error starting camera preview: "

+ e.getMessage());

}

}

}, 2000);

}

};

@Override

public void surfaceCreated(SurfaceHolder holder) {

try {

camera.setPreviewDisplay(holder);

camera.startPreview();

} catch (Exception e){

Log.d("camera", e.getMessage());

}

}

@Override

public void surfaceChanged(SurfaceHolder holder,

int format, int w, int h3) {

if (holder.getSurface() == null){

Log.d(TAG, "surface does not exist, return");

return;

}

try {

camera.setPreviewDisplay(holder);

camera.startPreview();

} catch (Exception e){

Log.d("camera", e.getMessage());

}

}

@Override

public void surfaceDestroyed(SurfaceHolder holder) {

Log.d(TAG, "surfaceDestroyed");

}

}

The MainActivity class uses a Camera and a SurfaceView. The latter continuously displays what the camera sees. Since a Camera takes a lot of resources to operate, the MainActivity releases the camera when the application stops and re-opens it when the application resumes.

The MainActivity class also implements SurfaceHolder.Callback and passes itself to the SurfaceHolder of the SurfaceView it employs as a viewfinder. This is shown in the following lines in the onCreate method.

SurfaceView surfaceView = (SurfaceView)

findViewById(R.id.surfaceview);

surfaceView.getHolder().addCallback(this);

In both surfaceCreated and surfaceChanged methods that MainActivity overrides, the class calls the camera’s setPreviewDisplay and startPreview methods. This makes sure when the camera is linked with a SurfaceHolder, the SurfaceHolder has already been created.

Another important point in MainActivity is the takePicture method that gets called when the user presses the button.

public void takePicture(View view) {

enableButton(false);

camera.takePicture(shutterCallback, null,

pictureCallback);

}

The takePicture method disables the button so that no more picture can be taken until the picture is saved and calls the takePicture method on the Camera, passing a Camera.ShutterCallback and a Camera.PictureCallback. Note that calling takePicture on a Camera also stops previewing the image on the SurfaceHolder linked to the camera.

The Camera.ShutterCallback in MainActivity has one method, onShutter, that plays a sound from the sound pool.

@Override

public void onShutter() {

// play sound

soundPool.play(beepId, 1.0f, 1.0f, 0, 0, 1.0f);

}

The Camera.PictureCallback also has one method, onPictureTaken, whose signature is this.

public void onPictureTaken(byte[] data, final Camera camera)

This method is called by the Camera’s takePicture method and receives a byte array containing the photo image.

The onPictureTaken method implementation in MainActivity does three things. First, it displays a message using the Toast. Second, it saves the byte array into a file. The name of the file is generated using System.currentTimeMillis(). Finally, the method creates a Handler to schedule a task that will be executed in two seconds. The task enables the button and calls the camera’s startPreview so that the viewfinder will start working again.

Figure 19.2 shows the CameraAPIDemo application.

image

Figure 19.2: The CameraAPIDemo application

Summary

Android offers two options for applications that need to take still pictures: use a built-in intent to start Camera or use the Camera API. The first option is the easier one to use but lacks the features that the Camera API provides.

This chapter showed how to use both methods.