I've found many questions here about android camera preview orientation problems. All the fixes involve fixing the screen orientation to landscape or calling camera.setDisplayOrientation(90) or calling params.setRotation(90). I can't find the combination of settings which actually make it work when held in landscape orientation.
The activity is fixed to landscape mode with android:screenOrientation="landscape".
My problem is that if I hold the camera in portrait orientation and start up the application it works fine (by which I mean that it properly shows the image as a landscape). If I hold the camera in landscape orientation when starting the application the picture is all messed up (sort of interlaced). If I use camera.setDisplayOrientation(90); the picture is no longer messed up, but the image is oriented sideways.
Strangely, if I remove the android:screenOrientation="landscape" and allow the screen to rotate, I still have the same problems, but if I rotate the phone to portrait it looks fine in portrait. If I rotate it back to landscape then it looks fine in landscape! My only problem is that it won't work correctly when it first starts up.
import java.util.List;
import android.content.Context;
import android.graphics.Color;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.util.AttributeSet;
import android.util.Log;
import android.view.*;
public class CameraView extends SurfaceView
{
//Callback for the surfaceholder
SurfaceHolder.Callback surfaceHolderListener = new SurfaceHolder.Callback() {
public void surfaceCreated(SurfaceHolder holder) {
try {
camera=Camera.open();
} catch(Exception e) {
Log.e("CameraView","Couldn't open the camera.",e);
}
try {
camera.setPreviewDisplay(previewHolder);
} catch (Throwable e) {
Log.e("CameraView","Couldn't call setPreviewDisplay.",e);
}
}
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int w, int h) {
if(camera!=null) {
Parameters params = camera.getParameters();
List<Size> sizes = params.getSupportedPreviewSizes();
Size optimalSize = getOptimalPreviewSize(sizes, w, h);
params.setPreviewSize(optimalSize.width, optimalSize.height);
camera.setParameters(params);
camera.startPreview();
}
}
public void surfaceDestroyed(SurfaceHolder arg0) {
if(camera!=null) {
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
}
}
private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.05;
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;
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
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;
}
};
public CameraView(Context ctx) {
super(ctx);
previewHolder = this.getHolder();
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
previewHolder.addCallback(surfaceHolderListener);
setBackgroundColor(Color.TRANSPARENT);
}
public CameraView(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
}
private Camera camera;
private SurfaceHolder previewHolder;
}
EDIT: The real problem seems to be that something happens after surfaceChanged which screws things up. I found that if I start the camera preview and then add a debug point at the end of surfaceChanged the camera looks fine. Then if I step forward about 20 steps it suddenly looks screwed up.
I solved this (in a way which I think is a complete hack) by using the orientation change listener to update the orientation exactly one time and then disable itself. I just needed something which would activate after the view was initially set up. I could probably have done it elsewhere.
If anyone has any idea how to fix this without having it be this completely stupid, please help!
/** This works fine for me, but it's a hack. */
import java.util.List;
import android.content.Context;
import android.graphics.Color;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class CameraView3
extends SurfaceView
{
private static Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.05;
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;
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
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;
}
private class CameraSurfaceHolder
extends OrientationEventListener
implements SurfaceHolder.Callback
{
CameraSurfaceHolder(Context ctx) {
super(ctx);
}
public void surfaceCreated(SurfaceHolder holder) {
try {
camera= Camera.open();
} catch(Exception e) {
Log.e("CameraView","Couldn't open the camera.",e);
}
try {
camera.setPreviewDisplay(previewHolder);
} catch (Throwable e) {
Log.e("CameraView","Couldn't call setPreviewDisplay.",e);
}
}
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int w, int h) {
}
public void surfaceDestroyed(SurfaceHolder arg0) {
if(camera!=null) {
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
}
}
@Override
public void onOrientationChanged(int orientation) {
if(camera!=null) {
DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
camera.stopPreview();
CameraView3.this.layout(0, 0, dm.widthPixels-1, dm.heightPixels-1);
camera.startPreview();
disable();
return;
}
}
}
public CameraView3(Context ctx) {
super(ctx);
this.ctx = ctx;
previewHolder = this.getHolder();
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
surfaceHolderListener = new CameraSurfaceHolder(ctx);
previewHolder.addCallback(surfaceHolderListener);
surfaceHolderListener.enable();
setBackgroundColor(Color.TRANSPARENT);
}
private Context ctx;
private Camera camera;
private SurfaceHolder previewHolder;
private CameraSurfaceHolder surfaceHolderListener;
private Size optimalSize;
}
In summary, the first version I posted worked fine on my Nexus One. But, it doesn't work on my Skyrocket. I need to use the second version to make it work there.