转载自:Penguin
Android Camera Develop: orientation/rotation and aspect ratio
概述接上篇,在实现了相机的基础功能后,着眼于解决预览、拍照与录像时屏幕的旋转,以及预览时的纵横比等问题。上篇实现的相机APP只能以横屏方向预览、拍照和录像,在实际使用时会很不方便;本篇就会实现APP随设备的旋转而旋转,且可以在任意旋转角度上进行预览、拍照和录像。上篇实现的相机APP中,预览画面是充满整个屏幕的,即使调整预览分辨率后,预览画面所占尺寸也不变,这样会造成实际画面被拉长或压扁,十分不友好;本篇会实现预览画面纵横比与分辨率纵横比保持相同,即预览画面不会出现拉长或压扁的情况。
预览画面随设备旋转如果只是简单地将AndroidManifest中横屏锁去掉,虽然可以实现APP随设备旋转,但预览画面不会随之旋转,如下图所示:
Screenshot Preview LandscapeScreenshot Preview Portrait Old原因主要在于Camera的预览显示方向需要单独设置,不会随着设备的旋转而自动改变。而通过设置Camera的预览显示方向,可以自由指定旋转的角度。
解除旋转锁定在AndroidManifest.xml中删去
XML
android:screenOrientation="landscape"这样就解除了之前设定的APP的旋转锁定(注意与设备的旋转锁定不同),现在APP可以随设备旋转而旋转了。
计算旋转角度相机预览的旋转角度需要根据相机预览目前的旋转角度,以及设备屏幕的旋转角度计算得到,不过还好Android官方给了示例代码。
在CameraPreview中加入
Java
public int getDisplayOrientation() { Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); int rotation = display.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; } android.hardware.Camera.CameraInfo camInfo = new android.hardware.Camera.CameraInfo(); android.hardware.Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, camInfo); int result = (camInfo.orientation - degrees + 360) % 360; return result;}getDisplayOrientation()用来获取相机预览需要旋转的角度。前面一部分获得设备屏幕的旋转角度,即由重力传感器自动旋转屏幕的角度;然后得到相机原先的旋转角度camInfo.orientation,最后通过运算得到新的相机预览需要旋转的角度。
预览画面随设备旋转设备可能经常在旋转,那什么时候将计算得到的旋转角度应用到相机呢?当然是要等待相应事件触发了。还记得之前一直没有派上用场的surfaceChanged()吗?现在就要用到它了,surfaceChanged()会在surface的大小或格式发生改变时调用,而屏幕的旋转恰恰会使得surface大小发生变化(横屏变为竖屏、竖屏变为横屏,surface的长宽都会发生变化),所以调整相机预览旋转角度就在这里了。
在surfaceChanged()中加入
Java
int rotation = getDisplayOrientation();mCamera.setDisplayOrientation(rotation);即surfaceChanged()触发后,计算相机预览需要旋转的角度rotation,再通过setDisplayOrientation()将此角度应用到Camera。
运行试试现在运行APP,试试旋转设备,相机预览也随之旋转了,不会再出现之前的怪异画面了。
Screenshot Preview Portrait New优化UI布局有没有发现上图中在竖屏下控制按钮在屏幕右边很别扭?这是因为竖屏下APP仍然应用的横屏下的布局,本意是为了让布局的相对位置不随旋转而改动,但在这里明显不满足我们的需求了。我们考虑为APP配置横屏和竖屏两个布局,让Android根据屏幕方向自动选择。
新建横屏布局文件双击打开activity_main.xml,点击面板左下角切换到Design界面,在如下图所示处点击Create Landscape Variation。
CreateLandscapeLayout这样在项目文件列表中,activity_main.xml就会变成一个文件夹,其内含有两个activity_main.xml文件,其中有(land)的是横屏布局文件,另一个是竖屏布局文件。
加入竖屏布局新建的横屏布局文件自动填充了之前的布局内容,所以横屏布局就不用修改了。
竖屏布局文件内容修改为:
XML
按照标准,不应该在android:text中直接写入文字,而应当在res/values/strings.xml中写入,在layout中引用。本APP实际也是这样写的,但此处为演示方便,还是直接在layout中写入
运行试试运行APP,此时在横屏下还是之前的布局,但在竖屏下已经应用新的布局了,如下图所示
Screenshot Preview Portrait New Layout拍照与录像随预览旋转现在预览和界面没问题了,但如果你拍张照,或者录像的话会发现照片或视频的方向没有随着预览方向而改变,也就是说不是“所见即所得”,现在来着手解决这个问题。
这个问题不是bug,而是Android故意的,我们也是需要手动指定拍照和录像的旋转角度。
修改拍照旋转角度照片的旋转角度是在Camera的Parameters中指定的。这里有一个很困惑的地方,我们之前设置的setDisplayOrientation()是直接对Camera操作的,指定预览的旋转角度;而在Parameters中,有个方法setRotation()同样也是设置旋转角度,这两个有什么区别呢?这里我们不作深入追究,可以理解为setRotation()是设置预览帧数据,以及拍摄照片的方向,而setDisplayOrientation()仅设置预览显示的方向。
所以现在要做的就是在修改预览显示旋转角度时,同时设置拍照旋转角度。在surfaceChanged()中加入
Java
Camera.Parameters parameters = mCamera.getParameters();parameters.setRotation(rotation);mCamera.setParameters(parameters);即首先获取Parameters,设置旋转角度,再将Parameters应用到Camera。
这样拍照得到的照片就和预览的方向一致了。
修改录像旋转角度因为录像是交给MediaRecorder实际实现的,所以应当是给MediaRecorder设置旋转参数。
在prepareVideoRecorder()中,mMediaRecorder.prepare()之前加入
Java
int rotation = getDisplayOrientation();mMediaRecorder.setOrientationHint(rotation);首先得到旋转角度,然后通过setOrientationHint()应用到MediaRecorder,只需要注意在prepare()前应用就好了。
这样录像得到的视频就和预览的方向一致了。
注意正如setOrientationHint()中的Hint所表示的,视频的旋转并不是编码层面的旋转,视频帧数据并没有发生旋转,而只是在视频中增加了参数,希望播放器按照指定的旋转角度旋转后播放,所以具体效果因播放器而异
实时调整预览纵横比Aspect Ratio如上图所示,黑色矩形框为屏幕,灰色矩形框为SurfaceView的父级FrameLayout。我们的APP现在是SurfaceView充满整个FrameLayout即灰色矩形框,而这样会造成实际画面被拉长或压扁。解决方法是将SurfaceView的纵横比与预览分辨率的纵横比调整为相同,这样从屏幕上看到拍摄到的物体与实际情况就只是产生了缩放,而不会出现变形。为了达到最好的显示效果,我们希望SurfaceView能尽可能充满FrameLayout,如图中红色和绿色矩形框所示。通过观察可以很快发现,如果预览分辨率的纵横比大于FrameLayout的纵横比,则将SurfaceView的纵向长度设定为FrameLayout的纵向长度,而SurfaceView的横向长度由纵横比计算得到,如图中绿色矩形框所示;反之如红色矩形框所示。
计算尺寸在CameraPreview中加入
Java
private void adjustDisplayRatio(int rotation) { ViewGroup parent = ((ViewGroup) getParent()); Rect rect = new Rect(); parent.getLocalVisibleRect(rect); int width = rect.width(); int height = rect.height(); Camera.Size previewSize = mCamera.getParameters().getPreviewSize(); int previewWidth; int previewHeight; if (rotation == 90 || rotation == 270) { previewWidth = previewSize.height; previewHeight = previewSize.width; } else { previewWidth = previewSize.width; previewHeight = previewSize.height; } if (width * previewHeight > height * previewWidth) { final int scaledChildWidth = previewWidth * height / previewHeight; layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height); } else { final int scaledChildHeight = previewHeight * width / previewWidth; layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2); }}首先得到SurfaceView的父级parent(在这里就是FrameLayout),记录父级的长和宽;然后得到预览分辨率(需要注意处理横屏和竖屏的问题),通过比较两者的纵横比,确定SurfaceView的调整方法,计算出需要调整的长度,并使SurfaceView居中;最后通过layout()将新的SurfaceView的位置应用到布局中,完成纵横比的调整。
应用新的尺寸预览分辨率的变化也会触发surfaceChanged(),所以调用adjustDisplayRatio()也是在surfaceChanged()中。在surfaceChanged()最后加入
Java
adjustDisplayRatio(rotation);运行试试运行APP,马上就能发现相机预览不再是充满整个屏幕了,而是在边界有一定的空白,这样就保持了与预览分辨率相同的纵横比。另外,如果在预览的设置中修改了预览分辨率,新的纵横比也会立即应用到屏幕显示中。如下所示
Screenshot Aspect Ratio美化以上完成了本篇需要实现的全部功能。这里提一点APP的美化,这里只是指出进行美化的地方,具体细节参见DEMO。
布局首先可以将整个布局背景设置为黑色,使布局空白地方为黑色,像电影一样。在activity_main中,LinearLayout中加入
XML
android:background="@color/black"其次将控制部分背景颜色区分开,同时将控制部分预留一部分边界,使布局结构明显。在activity_main中,RelativeLayout中加入
XML
android:background="@color/darkGray"android:padding="5dp"偏好设置文本颜色偏好设置文本颜色默认为黑色,对于相机预览来说不容易分辨,将文本颜色设置为白色更好。这里我们给SettingsFragment设置一个主题。
在res/valuse/styles.xml中,resources下加入
XML
#FFF #FFF创建一个名为PreferenceTheme的主题,主题只是修改文本颜色为纯白。
在SettingsFragment的onCreate()中,addPreferencesFromResource()之后加入
XML
getActivity().setTheme(R.style.PreferenceTheme);即应用此主题。
这样偏好设置文本颜色就变为白色了,容易区分多了吧!
一点唠叨本篇完成后,这个相机APP就离实用的APP更近了一步,甚至已经满足了大多数的需求。其实在旋转与纵横比的实现上,需要仔细研究诸如继承关系、生命周期等的问题,但