OK, I think that I figured this out. There were a few pieces to the proverbial puzzle, and I thank @kcoppock for the insight that helped me solve some of this.
First, you need to take the orientation into account when determining what preview size to choose. For example, the getOptimalPreviewSize()
from the CameraPreview
of ApiDemos
is oblivious to orientation, simply because their version of that app has the orientation locked to landscape. If you wish to let the orientation float, you need to reverse the target aspect ratio to match. So, where getOptimalPreviewSize()
has:
double targetRatio=(double)width / height;
you also need:
if (displayOrientation == 90 || displayOrientation == 270) {
targetRatio=(double)height / width;
}
where displayOrientation
is a value from 0 to 360, that I am determining from about 100 lines of some seriously ugly code, which is why I am wrapping all of this up in a reusable component that I'll publish shortly. That code is based on the setDisplayOrientation()
JavaDocs and this StackOverflow answer.
(BTW, if you are reading this after 1 July 2013, and you don't see a link in this answer to that component, post a comment to remind me, as I forgot)
Second, you need to take that display orientation into account when controlling the aspect ratio of the SurfaceView
/TextureView
that you are using. The CameraPreview
activity from ApiDemos
has its own Preview
ViewGroup
that handles the aspect ratio, and there you need to reverse the aspect ratio for use in portrait:
if (displayOrientation == 90
|| displayOrientation == 270) {
previewWidth=mPreviewSize.height;
previewHeight=mPreviewSize.width;
}
else {
previewWidth=mPreviewSize.width;
previewHeight=mPreviewSize.height;
}
where displayOrientation
is that same value (90
and 270
being portrait and reverse-portrait respectively, and note that I haven't tried getting reverse-portrait or reverse-landscape to work, so there may be more tweaking required).
Third -- and one that I find infuriating -- you must start the preview before calling setPictureSize()
on the Camera.Parameters
. Otherwise, it's as if the aspect ratio of the picture is applied to the preview frames, screwing things up.
So I used to have code resembling the following:
Camera.Parameters parameters=camera.getParameters();
Camera.Size pictureSize=getHost().getPictureSize(parameters);
parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
parameters.setPictureSize(pictureSize.width, pictureSize.height);
parameters.setPictureFormat(ImageFormat.JPEG);
camera.setParameters(parameters);
camera.startPreview();
and that's wrong. What you really need is:
Camera.Parameters parameters=camera.getParameters();
Camera.Size pictureSize=getHost().getPictureSize(parameters);
parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
camera.setParameters(parameters);
camera.startPreview();
parameters=camera.getParameters();
parameters.setPictureSize(pictureSize.width, pictureSize.height);
parameters.setPictureFormat(ImageFormat.JPEG);
camera.setParameters(parameters);
Without that change, I'd get a stretched aspect ratio, even in landscape. This wasn't a problem for CameraPreview
of ApiDemos
, as they were not taking pictures, and so they never called setPictureSize()
.
But, so far, on a Nexus 4, I now have square aspect ratios for portrait and landscape, with a floating orientation (i.e., not locked to landscape).
I'll try to remember to amend this question if I run into other hacks required by other devices, plus to link to my CameraView
/CameraFragment
components when they are released.
UPDATE #1
Well, of course, it couldn't possibly by nearly this simple. :-)
The fix for the problem where setPictureSize()
screws up the aspect ratio works... until you take a picture. At that point, the preview switches to the wrong aspect ratio.
One possibility would be to limit pictures to ones with the same (or very close) aspect ratio to the preview, so the hiccup does not exist. I don't like that answer, as the user should be able to get whatever picture size the camera offers.
You can limit the scope of the damage by:
- Only updating the picture size, etc. of the
Camera.Parameters
just before taking the picture
- Reverting to the pre-picture-taking
Camera.Parameters
after taking the picture
This still presents the wrong aspect ratio during the moments while the picture is being taken. The long-term solution for this -- I think -- will be to temporarily replace the camera preview with a still image (the last preview frame) while the picture is being taken. I'll try this eventually.
UPDATE #2: v0.0.1 of my CWAC-Camera library is now available, incorporating the aforementioned code.