A task that is (or should be) rather simple is to show a camera preview in your App. There is a very nice example that comes with the Android SDK - however, it has a couple of problems: first, it is written for Gingerbread (2.3) – meaning you cannot use it if your app also targets older versions (which it has to). Second, it only works in landscape orientation, so your preview will be rotated if you hold your phone in portrait mode.
Here’s my solution, which builds on the example from Google, but also supports older phones, and takes into account the device orientation.One thing to note is that you need to extract the camera handling in 2 separate classes, one for Froyo (2.2) and older, one for Gingerbread (2.3) and newer. The reason is the Camera.CameraInfo class, which was introduced with 2.3 – if your class has at least an import referring to the CameraInfo class, you’ll get runtime exceptions when running on an older platform. That’s why we need the CameraPreviewProvider interface, along with its 2 implementations.
Here’s the CameraPreview component.
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
SurfaceHolder holder;
Camera camera;
private int lastReportedWidth;
private int lastReportedHeight;
private static final String TAG = "CameraPreviewLog";
private CameraPreviewProvider previewProvider;
public CameraPreview(Context context, AttributeSet attrs) {
super(context, attrs);
holder = getHolder();
holder.addCallback(this);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
previewProvider = new CameraPreviewGingerbread();
} else {
previewProvider = new CameraPreviewFroyo();
}
}
public void surfaceCreated(SurfaceHolder holder) {
preparePreview();
}
public void surfaceDestroyed(SurfaceHolder holder) {
stopPreview();
}
private void preparePreview() {
try {
camera = previewProvider.openCamera();
camera.setPreviewDisplay(holder);
} catch (Exception exception) {
if (camera != null) {
camera.release();
}
camera = null;
}
}
private void stopPreview() {
if (camera != null) {
camera.stopPreview();
camera.release();
camera = null;
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
this.lastReportedWidth = w;
this.lastReportedHeight = h;
if (camera != null) {
startPreview();
}
}
private void startPreview() {
Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
previewProvider.startPreview(camera, lastReportedWidth,
lastReportedHeight, display);
}
}
And the CameraPreviewProvider interface
interface CameraPreviewProvider {
Camera openCamera();
void startPreview(Camera camera, int previewWidth, int previewHeight, Display display);
}
The implementation for Froyo (2.2) and older
class CameraPreviewFroyo implements CameraPreviewProvider {
public Camera openCamera() {
return Camera.open();
}
public void startPreview(Camera camera, int previewWidth, int previewHeight, Display display) {
Camera.Parameters parameters = camera.getParameters();
List supportedPreviewSizes = parameters.getSupportedPreviewSizes();
Size optimalPreviewSize = CameraPreviewFroyo.getOptimalPreviewSize(supportedPreviewSizes, previewWidth, previewHeight);
if (optimalPreviewSize != null) {
parameters.setPreviewSize(optimalPreviewSize.width, optimalPreviewSize.height);
camera.setParameters(parameters);
camera.startPreview();
}
}
static Size getOptimalPreviewSize(List sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.1;
final double MAX_DOWNSIZE = 1.5;
double targetRatio = (double) w / h;
if (sizes == null) return null;
Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
int targetHeight = h;
// Try to find an size match aspect ratio and size
for (Size size : sizes) {
double ratio = (double) size.width / size.height;
double downsize = (double) size.width / w;
if (downsize > MAX_DOWNSIZE) {
//if the preview is a lot larger than our display surface ignore it
//reason - on some phones there is not enough heap available to show the larger preview sizes
continue;
}
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
// Cannot find the one match the aspect ratio, ignore the requirement
//keep the max_downsize requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
double downsize = (double) size.width / w;
if (downsize > MAX_DOWNSIZE) {
continue;
}
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
//everything else failed, just take the closest match
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
}
And the implementation for Gingerbread (2.3) and newer.
class CameraPreviewGingerbread implements CameraPreviewProvider {
private int cameraId = -1;
private int cameraOrientation;
public Camera openCamera() {
CameraInfo cameraInfo = new CameraInfo();
for (int i = 0; i < Camera.getNumberOfCameras(); i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
cameraId = i;
cameraOrientation = cameraInfo.orientation;
return Camera.open(cameraId);
}
}
return null;
}
public void startPreview(Camera camera, int previewWidth, int previewHeight, Display display) {
int displayRotation = display.getRotation();
Camera.Parameters parameters = camera.getParameters();
List supportedPreviewSizes = parameters.getSupportedPreviewSizes();
Size optimalPreviewSize = CameraPreviewFroyo.getOptimalPreviewSize(supportedPreviewSizes, previewWidth, previewHeight);
if (optimalPreviewSize != null) {
int width = optimalPreviewSize.width, height = optimalPreviewSize.height;
int degrees = 0;
switch (displayRotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:
degrees = 270;
break;
}
int result = (cameraOrientation - degrees + 360) % 360;
camera.setDisplayOrientation(result);
parameters.setPreviewSize(width, height);
camera.setParameters(parameters);
camera.startPreview();
}
}
}
This has been tested on a variety of devices and it works correctly, however if you find any problems on other devices, please do let me know about them. One device where it does not work 100% correctly is the Sony Ericsson E10i / Xperia Mini – there the preview appears rotated, if the phone is held in portrait mode. However, there is no api that allows us to rotate the preview, as it runs with Eclair (2.1).
When showing the Camera Preview it’s best to use a full-screen activity, because then your preview dimensions will be closer to the native camera resolution. That’s why the default Camera app runs fullscreen.
You can download the complete source code as an Eclipse project here: CameraPreview