問題:最近在處理一下camera的問題,發現在豎屏時預覽圖像會變形,而橫屏時正常。但有的手機則是橫豎屏都會變形。
結果:解決了預覽變形的問題,同時支持前后攝像頭,預覽無變形,拍照生成的jpg照片方向正確。
環境 : android studio, xiaomi m1s android4.2 miui v5
過程:
1、預覽 preview畫面變形
以sdk中apidemos里的camera為例,進行修改。先重現下問題,在AndroidManifest.xml中指定了activity 的screenOrientation為landspace,則預覽正常。若指定為portrait,則圖像會有拉伸變形。
找到正確的previewsize
繼續看demo代碼,mCamera.getParameters().getSupportedPreviewSizes() 可以返回當前設備支持的一組previewSize,例如:
1920x1088 1280x720 800x480 768x432 720x480 640x480 576x432 480x320
384x288 352x288 320x240 240x160 176x144
而我們根據我們在界面上需要顯示的預覽大小,來設置camera的預覽大小,即在這一組size中選擇一個previewsize,找到高寬比和大小最接近的一個size。通過調用 getOptimalPreviewSize(List sizes, int w, int h) 來進行選擇。注意這里的后兩個參數,因為攝像頭的預覽size是固定的,就那么一組,其高寬比是固定的,且方向也是固定的。
對於攝像頭來說,都是width是長邊,即width > height。 所以camera的ratio計算值總是大於1的。所以當手機在橫屏的時候,我們的w>h,調用該方法進行選擇是沒問題的。但是當豎屏后,w < h了,若還是直接調用該方法,則targetRatio 會小於1,按這個targetRatio去找就找不到合適的size了,那么比例不對預覽自然就變形了。所以得在調用的地方進行調整,保證參數w > h。
private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.1;
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;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// We purposely disregard child measurements because act as a
// wrapper to a SurfaceView that centers the camera preview instead
// of stretching it.
final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
setMeasuredDimension(width, height);
if (mSupportedPreviewSizes != null) {
mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, Math.max(width, height), Math.min(width, height));
}
......
選擇和previewsize一致的比例來布局surfaceview
當我們選擇了合適的previewsize后,還有一個因素會影響到預覽畫面是否正常。即surfaceview的布局。從demo代碼可以知道要想在界面上顯示camera預覽畫面,需要添加一個surfaceview,而我們添加了surfaceview后,就需要對其進行布局,設置其大小,位置。
經過搜索查資料,here這里有人回答了原因。
“The reason is: SurfaceView aspect ratio (width/height) MUST be same as Camera.Size aspect ratio used in preview parameters. And if aspect
ratio is not the same, you've got stretched image.”
看到這句話,理解了變形是因為比例錯誤。surfaceview和cameraSize的比例應該要一致。
在onlayout中,我們對surfaceview進行了layout,根據指定的surfaceview的高寬來布局。demo中會將surfacevidew居中,看代碼是根據寬高比和previewsize的寬高比來對比,選擇水平居中或垂直居中。前面已經說過,previewsize的width大於height,所以凡是涉及到寬高比計算的地方,兩個size我們都需要保持同樣的順序,都是w>h,或w
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int curOrientation =
getContext().getResources().getConfiguration().orientation;
if (changed && getChildCount() > 0 || mLastOrientation != curOrientation) {
mLastOrientation = curOrientation;
final View child = getChildAt(0);
final int width = r - l;
final int height = b - t;
int previewWidth = width;
int previewHeight = height;
if (mPreviewSize != null) {
previewWidth = mPreviewSize.width;
previewHeight = mPreviewSize.height;
if (curOrientation == Configuration.ORIENTATION_PORTRAIT) {
previewWidth = mPreviewSize.height;
previewHeight = mPreviewSize.width;
}
}
// Center the child SurfaceView within the parent.
if (width * previewHeight > height * previewWidth) {
final int scaledChildWidth = previewWidth * height / previewHeight;
child.layout((width - scaledChildWidth) / 2, 0,
(width + scaledChildWidth) / 2, height);
} else {
final int scaledChildHeight = previewHeight * width / previewWidth;
child.layout(0, (height - scaledChildHeight) / 2,
width, (height + scaledChildHeight) / 2);
}
}
}
至此,經過測試,設備顯示正常,橫屏豎屏均再無拉伸現象了。
總結下,demo工程在橫屏下正常,而在豎屏下出現預覽畫面變形的原因主要是,onMeasure時選擇previewsize和onlayout時布局surfaceview,都是基於橫屏考慮的,所以w均大於h。
當activity改為豎屏運行時,就需要調整這兩個地方,保證比例一致,才能計算正確,從而顯示正常畫面。
2、方向問題
剛剛講了變形的問題,我們應該發現當手機豎屏后,預覽畫面的方向沒有隨之改變過來,於是看上去就顛倒了。所以我們還需要處理一下方向的問題。
注意這里的“方向”包括:前置、后置攝像頭畫面預覽方向,前置后置攝像頭拍照后的圖片方向。
預覽方向:
通過查詢android文檔,可以發現如下資料:
For example, suppose the natural orientation of the device is
portrait. The device is rotated 270 degrees clockwise, so the device
orientation is 270. Suppose a back-facing camera sensor is mounted in
landscape and the top side of the camera sensor is aligned with the
right edge of the display in natural orientation. So the camera
orientation is 90. The rotation should be set to 0 (270 + 90).
(后置攝像頭)
可以得知,camera是在設置上是固定方向的, camera的頂部是和屏幕自然顯示時的右邊對齊的。說明camera默認就是橫屏方向的。為了在豎屏的時候進行preview預覽,我們需要調整camera的方向。
setDisplayOrientation 可以修改camera的預覽方向。
If you want to make the camera image show in the same orientation as
the display, you can use the following code.
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
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;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
拍照方向:
若想修改照片的方向,還需調用 camera.setRotation,google 的文檔說的很清楚了,添加一個orientation listener既可以從傳感器獲取當前設備旋轉方向,參考如下代碼即可正確設置照片方向。但要注意一點,這個方向和activity方向(可以在android-manifest設置)無關。 無論activity此時是什么方向,只要獲取了傳感器方向均可以正確調整照片方向,與預覽方面一致。
代碼方面,由於這個回調調用比較頻繁(設備角度一變化就會調用),可以在回調里保存下rotation,然后在拍照的時候再設置camera。由於考慮了前置攝像頭,須注意傳遞正確的cameraId。
mCamera.setParameters(parameters);
mCamera.takePicture(shutterCallback, rawCallback,jpegCallback);
CameraInfo.orientation is the angle between camera orientation and
natural device orientation. The sum of the two is the rotation angle
for back-facing camera. The difference of the two is the rotation
angle for front-facing camera. Note that the JPEG pictures of
front-facing cameras are not mirrored as in preview display. For
example, suppose the natural orientation of the device is portrait.
The device is rotated 270 degrees clockwise, so the device orientation
is 270. Suppose a back-facing camera sensor is mounted in landscape
and the top side of the camera sensor is aligned with the right edge
of the display in natural orientation. So the camera orientation is
90. The rotation should be set to 0 (270 + 90).The reference code is as follows.
public void onOrientationChanged(int orientation) {
if (orientation == ORIENTATION_UNKNOWN) return;
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
orientation = (orientation + 45) / 90 * 90;
int rotation = 0;
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else { // back-facing camera
rotation = (info.orientation + orientation) % 360;
}
mParameters.setRotation(rotation);
}
3、總結
到此為止,我們基本上是實現了一個最簡單的拍照應用,能支持前后攝像頭,預覽正確,照片方向正確。文中一些api在低版本sdk上沒有,不能直接用,還須參考資料換用其他方法。
由於手頭設備有限,我僅僅在android4.2 小米手機上測試過,pad未測試。
補充
setRotation 在一些設備上無效 拍照后得到的圖像還是橫屏的,所以在save前需要再rotate一下。
private Bitmap adjustPhotoRotationToPortrait(byte[] data) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, options);
if (options.outHeight < options.outWidth) {
int w = options.outWidth;
int h = options.outHeight;
Matrix mtx = new Matrix();
mtx.postRotate(90);
// Rotating Bitmap
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
Bitmap rotatedBMP = Bitmap.createBitmap(bmp, 0, 0, w, h, mtx, true);
return rotatedBMP;
} else {
return null;
}
}