当我按照官网给出的例子完成camera程序后,我发现这么几个问题:
1. 从预览界面看到的图像,是实际景象逆时针旋转后的图像;
2. 第一个问题解决后,拍出来的照片依然是被逆时针旋转了90度的图像;
3. 第二个问题也解决后,我发现拍出来的照片虽然方向对了,但没有铺满全屏,换言之,图像比例与屏幕比例不一致。
为解决上面的问题,下面的这几个概念就必须要先搞清楚:
ScreenOrientation 视图的方向
CameraOrientation 摄像头的挂载方向
PreviewSize 预览界面分辨率
PictureSize 最终获取到的图片的分辨率
一些基本概念
自然方向(natrual orientation)
每个设备都有一个自然方向,手机和平板的自然方向不同。android:screenOrientation的默认值unspecified即为自然方向。
关于orientation的两个常见值是这样定义的:
landscape(横屏):the display is wider than it is tall,正常拿着设备的时候,宽比高长,这是平板的自然方向。
portrait(竖屏):the display is taller than it is wide,正常拿设备的时候,宽比高短,这是手机的自然方向。
orientation的值直接影响了Activity.getWindowManager().getDefaultDisplay()的返回值:
activity在landscape下,w*h=1280*720,那么在portrait下就是w*h=720*1280
角度(angle)
所谓屏幕和摄像头角度,都是指相对于自然方向旋转过的角度。根据旋转角度即可获知当前的方向。
保持预览界面与实景一致
对于手机,其自然方向是portrait(0度),而camera默认情况下相对自然方向逆时针旋转了90度,即横屏模式。因此当设置android:screenOrientation=”landscape”时,预览界面的图像与实景方向是一致的。如果是portrait,那么看到的预览界面是逆时针旋转90后的景象。
通过Camera.setDisplayOrientation方法来使预览界面与实景保持一致的方法,官方文档已经给出:
public static void setCameraDisplayOrientation(int cameraId, android.hardware.Camera camera) { android.hardware.Camera.CameraInfo = 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);}
如果没有限定activity的方向,那么就是用上面的方法在每次屏幕方向改变后调用。
在部分手机上,如果限定了activity方向,比如portrait,那么这个函数返回的是定值90度。不论前置还是后置摄像头皆可一句话搞定:camera.setDisplayOrientation(90);
计算原理是这样的:
Activity.getWindowManager().getDefaultDisplay().getRotation();
视图转向与设备物理转向刚好相反。比如设备逆时针旋转了90度,系统计算的时候是考虑设备要顺时针旋转多少度才能恢复到自然方向,当然是再顺时针旋转90度,因此该方法此时返回的是90,即Surface.ROTATION_90;如果顺时针旋转90度,那么设备要顺时针旋转270度才能恢复到自然方向,因此返回值为270。因此,想快速计算出getRotation的返回值就这么想:设备逆时针旋转过的角度。手机在portrait下,该方法返回的是0度。
Camera.CameraInfo.orientation
该值是摄像头与自然方向的角度差,即摄像头需要顺时针转过多少度才能恢复到自然方向。如果屏幕是portrait,那么后置摄像头是90度,前置摄像头是270度,这样算下来,最后的result都是90度。
在不同的设备上,Camera.CameraInfo.orientation可能会有差异,所以最好通过上面的函数进行计算,而非设置定值。
在API level14之前,设置预览界面必须要按照如下顺序:Camera.stopPreview——>Camera.setDisplayOrientation——>Camera.startPreview。
保持照片与实景一致
仅仅设置预览界面与实景一致是不够的,还需通过Camera.Parameters.setRotation设置camera旋转的角度,看一下下面两个函数的对比:
Camera.setDisplayOrientation:设置camera的预览界面旋转的角度。该函数会影响预览界面的显示方向,对于竖屏模式的应用非常有用。这个函数不会影响最终拍照所得媒体文件的方向。
Camera.Parameters.setRotation:设置camera顺时针旋转角度。这个函数会影响最终拍出的图片方向。
计算setRotation需要旋转多少度,需要借助OrientationEventListener和Camera.CameraInfo.orientation。首先继承OrientationEventListener,实现onOrientationChanged方法。onOrientationChanged传入的角度是当前设备相对于自然方向顺时针旋转过的角度。OrientationEventListener与Camera.CameraInfo.orientation之和即为后置摄像头应该旋转的角度,差为前置摄像头应该旋转的角度。
官方文档也给出了计算的sample:
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); }
这样,拍照后得到的就是正常的图片。
找出最佳预览分辨率
这一步不是必须的。预览界面默认的分辨率可能不是最佳的,这样在拍照的时候,看到的实景就会不够清晰,最好的情况是找到与屏幕分辨率相同的预览分辨率。注意,设置该值不会影响最终拍出的图片的分辨率。
下面是从小米2S上获得的一组数据:
D/mycamera﹕ open the back camera
D/mycamera﹕ screen resolution 720*1280 D/mycamera﹕ camera default resolution 640x480 V/mycamera﹕ Supported preview resolutions: 1920x1088 1280x720 960x720 800x480 720x480 768x432 640x480 576x432 480x320 384x288 352x288 320x240 240x160 176x144 D/mycamera﹕ found preview resolution exactly matching screen resolutions: Point(1280, 720)首先我们要先获取camera支持的分辨率:Camera.getParameters().getSupportedPreviewSizes();
其次,我们排除分辨率过小的值,这个下限值是个经验值,不一定适合所有的手机;
再次我们尽可能取与屏幕分辨率接近的预览分辨率;
如果找不到合适的,就仍使用默认值。
下面是一个获取最佳预览分辨率的函数:
/** * 最小预览界面的分辨率 */ private static final int MIN_PREVIEW_PIXELS = 480 * 320; /** * 最大宽高比差 */ private static final double MAX_ASPECT_DISTORTION = 0.15;
/** * 找出最适合的预览界面分辨率 * * @return */ private Point findBestPreviewResolution() { Camera.Size defaultPreviewResolution = cameraParameters.getPreviewSize(); Log.d(TAG, "camera default resolution " + defaultPreviewResolution.width + "x" + defaultPreviewResolution.height); ListrawSupportedSizes = cameraParameters.getSupportedPreviewSizes(); if (rawSupportedSizes == null) { Log.w(TAG, "Device returned no supported preview sizes; using default"); return new Point(defaultPreviewResolution.width, defaultPreviewResolution.height); } // 按照分辨率从大到小排序 List supportedPreviewResolutions = new ArrayList (rawSupportedSizes); Collections.sort(supportedPreviewResolutions, new Comparator () { @Override public int compare(Camera.Size a, Camera.Size b) { int aPixels = a.height * a.width; int bPixels = b.height * b.width; if (bPixels < aPixels) { return -1; } if (bPixels > aPixels) { return 1; } return 0; } }); StringBuilder previewResolutionSb = new StringBuilder(); for (Camera.Size supportedPreviewResolution : supportedPreviewResolutions) { previewResolutionSb.append(supportedPreviewResolution.width).append('x').append(supportedPreviewResolution.height) .append(' '); } Log.v(TAG, "Supported preview resolutions: " + previewResolutionSb); // 移除不符合条件的分辨率 double screenAspectRatio = (double) screenResolution.x / (double) screenResolution.y; Iterator it = supportedPreviewResolutions.iterator(); while (it.hasNext()) { Camera.Size supportedPreviewResolution = it.next(); int width = supportedPreviewResolution.width; int height = supportedPreviewResolution.height; // 移除低于下限的分辨率,尽可能取高分辨率 if (width * height < MIN_PREVIEW_PIXELS) { it.remove(); continue; } // 在camera分辨率与屏幕分辨率宽高比不相等的情况下,找出差距最小的一组分辨率 // 由于camera的分辨率是width>height,我们设置的portrait模式中,width height; int maybeFlippedWidth = isCandidatePortrait ? height : width; int maybeFlippedHeight = isCandidatePortrait ? width : height; double aspectRatio = (double) maybeFlippedWidth / (double) maybeFlippedHeight; double distortion = Math.abs(aspectRatio - screenAspectRatio); if (distortion > MAX_ASPECT_DISTORTION) { it.remove(); continue; } // 找到与屏幕分辨率完全匹配的预览界面分辨率直接返回 if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) { Point exactPoint = new Point(width, height); Log.d(TAG, "found preview resolution exactly matching screen resolutions: " + exactPoint); return exactPoint; } } // 如果没有找到合适的,并且还有候选的像素,则设置其中最大比例的,对于配置比较低的机器不太合适 if (!supportedPreviewResolutions.isEmpty()) { Camera.Size largestPreview = supportedPreviewResolutions.get(0); Point largestSize = new Point(largestPreview.width, largestPreview.height); Log.d(TAG, "using largest suitable preview resolution: " + largestSize); return largestSize; } // 没有找到合适的,就返回默认的 Point defaultResolution = new Point(defaultPreviewResolution.width, defaultPreviewResolution.height); Log.d(TAG, "No suitable preview resolutions, using default: " + defaultResolution); return defaultResolution; }
找出最佳图片分辨率
拍出来的照片,通过Camear.getParameters().setPictureSize()来设置。同preview一样,如果不设置图片分辨率,也是使用的默认的分辨率,如果与屏幕分辨率比例不一致,或不太相近,就会出现照片无法铺满全屏的情况。下面是小米2S上的一组数据:
D/mycamera﹕ Supported picture resolutions: 3264x2448 3264x1840 2592x1936 2048x1536 1920x1088 1600x1200 1280x768 1280x720 1024x768 800x600 800x480 720x480 640x480 352x288 320x240 176x144
D/mycamera﹕ default picture resolution 640x480 D/mycamera﹕ using largest suitable picture resolution: Point(3264, 1840) D/mycamera﹕ set the picture resolution 3264x1840找出最佳图片分辨率的方法与找出preview的方法非常类似:
private Point findBestPictureResolution() { ListsupportedPicResolutions = cameraParameters.getSupportedPictureSizes(); // 至少会返回一个值 StringBuilder picResolutionSb = new StringBuilder(); for (Camera.Size supportedPicResolution : supportedPicResolutions) { picResolutionSb.append(supportedPicResolution.width).append('x').append(supportedPicResolution.height).append(" "); } Log.d(TAG, "Supported picture resolutions: " + picResolutionSb); Camera.Size defaultPictureResolution = cameraParameters.getPictureSize(); Log.d(TAG, "default picture resolution " + defaultPictureResolution.width + "x" + defaultPictureResolution.height); // 排序 List sortedSupportedPicResolutions = new ArrayList (supportedPicResolutions); Collections.sort(sortedSupportedPicResolutions, new Comparator () { @Override public int compare(Camera.Size a, Camera.Size b) { int aPixels = a.height * a.width; int bPixels = b.height * b.width; if (bPixels < aPixels) { return -1; } if (bPixels > aPixels) { return 1; } return 0; } }); // 移除不符合条件的分辨率 double screenAspectRatio = (double) screenResolution.x / (double) screenResolution.y; Iterator it = sortedSupportedPicResolutions.iterator(); while (it.hasNext()) { Camera.Size supportedPreviewResolution = it.next(); int width = supportedPreviewResolution.width; int height = supportedPreviewResolution.height; // 在camera分辨率与屏幕分辨率宽高比不相等的情况下,找出差距最小的一组分辨率 // 由于camera的分辨率是width>height,我们设置的portrait模式中,width height; int maybeFlippedWidth = isCandidatePortrait ? height : width; int maybeFlippedHeight = isCandidatePortrait ? width : height; double aspectRatio = (double) maybeFlippedWidth / (double) maybeFlippedHeight; double distortion = Math.abs(aspectRatio - screenAspectRatio); if (distortion > MAX_ASPECT_DISTORTION) { it.remove(); continue; } } // 如果没有找到合适的,并且还有候选的像素,对于照片,则取其中最大比例的,而不是选择与屏幕分辨率相同的 if (!sortedSupportedPicResolutions.isEmpty()) { Camera.Size largestPreview = sortedSupportedPicResolutions.get(0); Point largestSize = new Point(largestPreview.width, largestPreview.height); Log.d(TAG, "using largest suitable picture resolution: " + largestSize); return largestSize; } // 没有找到合适的,就返回默认的 Point defaultResolution = new Point(defaultPictureResolution.width, defaultPictureResolution.height); Log.d(TAG, "No suitable picture resolutions, using default: " + defaultResolution); return defaultResolution; }
按钮随手机旋转自动旋转
在写MyCamera的过程中,顺便模仿小米相机按钮随根据手机方向自动旋转方向的效果。这个逻辑只需要在OrientationEventListener.onOrientationChanged中进行即可:
private class MyOrientationEventListener extends OrientationEventListener { public MyOrientationEventListener(Context context) { super(context); } @Override public void onOrientationChanged(int orientation) { if (orientation == ORIENTATION_UNKNOWN) return; ... ... // 使按钮随手机转动方向旋转 // 按钮图片的旋转方向应当与手机的旋转方向相反,这样最终方向才能保持一致 int phoneRotation = 0; if (orientation > 315 && orientation <= 45) { phoneRotation = 0; } else if (orientation > 45 && orientation <= 135) { phoneRotation = 90; } else if (orientation > 135 && orientation <= 225) { phoneRotation = 180; } else if (orientation > 225 && orientation <= 315) { phoneRotation = 270; } // 恢复自然方向时置零 if (phoneRotation == 0 && lastBtOrientation == 360) { lastBtOrientation = 0; } // "就近处理":为了让按钮旋转走"捷径",如果起始角度与结束角度差超过180,则将为0的那个值换为360 if ((phoneRotation == 0 || lastBtOrientation == 0) && (Math.abs(phoneRotation - lastBtOrientation) > 180)) { phoneRotation = phoneRotation == 0 ? 360 : phoneRotation; lastBtOrientation = lastBtOrientation == 0 ? 360 : lastBtOrientation; } if (phoneRotation != lastBtOrientation) { int fromDegress = 360 - lastBtOrientation; int toDegrees = 360 - phoneRotation; Log.i(TAG, "fromDegress=" + fromDegress + ", toDegrees=" + toDegrees); RotateAnimation animation = new RotateAnimation(fromDegress, toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setDuration(1000); animation.setFillAfter(true); buttonAnimation.executeAnimation(animation); lastBtOrientation = phoneRotation; } } }