转载自:Penguin
Android Camera Develop: orientation/rotation and aspect ratio
概述
接上篇,在实现了相机的基础功能后,着眼于解决预览、拍照与录像时屏幕的旋转,以及预览时的纵横比等问题。上篇实现的相机APP只能以横屏方向预览、拍照和录像,在实际使用时会很不方便;本篇就会实现APP随设备的旋转而旋转,且可以在任意旋转角度上进行预览、拍照和录像。上篇实现的相机APP中,预览画面是充满整个屏幕的,即使调整预览分辨率后,预览画面所占尺寸也不变,这样会造成实际画面被拉长或压扁,十分不友好;本篇会实现预览画面纵横比与分辨率纵横比保持相同,即预览画面不会出现拉长或压扁的情况。
预览画面随设备旋转
如果只是简单地将AndroidManifest中横屏锁去掉,虽然可以实现APP随设备旋转,但预览画面不会随之旋转,如下图所示:
原因主要在于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,试试旋转设备,相机预览也随之旋转了,不会再出现之前的怪异画面了。
优化UI布局
有没有发现上图中在竖屏下控制按钮在屏幕右边很别扭?这是因为竖屏下APP仍然应用的横屏下的布局,本意是为了让布局的相对位置不随旋转而改动,但在这里明显不满足我们的需求了。我们考虑为APP配置横屏和竖屏两个布局,让Android根据屏幕方向自动选择。
新建横屏布局文件
双击打开activity_main.xml
,点击面板左下角切换到Design
界面,在如下图所示处点击Create Landscape Variation
。
这样在项目文件列表中,activity_main.xml
就会变成一个文件夹,其内含有两个activity_main.xml
文件,其中有(land)
的是横屏布局文件,另一个是竖屏布局文件。
加入竖屏布局
新建的横屏布局文件自动填充了之前的布局内容,所以横屏布局就不用修改了。
竖屏布局文件内容修改为:
XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="设置" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="horizontal">
<Button
android:id="@+id/button_capture_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照" />
<Button
android:id="@+id/button_capture_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="录像" />
</LinearLayout>
<ImageView
android:id="@+id/media_preview"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:background="#000"
android:visibility="visible" />
</RelativeLayout>
</LinearLayout>
按照标准,不应该在android:text
中直接写入文字,而应当在res/values/strings.xml
中写入,在layout中引用。本APP实际也是这样写的,但此处为演示方便,还是直接在layout中写入
运行试试
运行APP,此时在横屏下还是之前的布局,但在竖屏下已经应用新的布局了,如下图所示
拍照与录像随预览旋转
现在预览和界面没问题了,但如果你拍张照,或者录像的话会发现照片或视频的方向没有随着预览方向而改变,也就是说不是“所见即所得”,现在来着手解决这个问题。
这个问题不是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所表示的,视频的旋转并不是编码层面的旋转,视频帧数据并没有发生旋转,而只是在视频中增加了参数,希望播放器按照指定的旋转角度旋转后播放,所以具体效果因播放器而异
实时调整预览纵横比
如上图所示,黑色矩形框为屏幕,灰色矩形框为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,马上就能发现相机预览不再是充满整个屏幕了,而是在边界有一定的空白,这样就保持了与预览分辨率相同的纵横比。另外,如果在预览的设置中修改了预览分辨率,新的纵横比也会立即应用到屏幕显示中。如下所示
美化
以上完成了本篇需要实现的全部功能。这里提一点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
<style name="PreferenceTheme">
<item name="android:textColor">#FFF</item>
<item name="android:textColorSecondary">#FFF</item>
</style>
创建一个名为PreferenceTheme
的主题,主题只是修改文本颜色为纯白。
在SettingsFragment
的onCreate()
中,addPreferencesFromResource()
之后加入
XML
getActivity().setTheme(R.style.PreferenceTheme);
即应用此主题。
这样偏好设置文本颜色就变为白色了,容易区分多了吧!
一点唠叨
本篇完成后,这个相机APP就离实用的APP更近了一步,甚至已经满足了大多数的需求。其实在旋转与纵横比的实现上,需要仔细研究诸如继承关系、生命周期等的问题,但本文没有谈及这些内容,还望想要了解其中细节的读者自己去研究。在纵横比的实现中,我采用的是在子类中获取父类,整个过程在子类中完成;但典型的方法是在父类中获取子类,整个过程在父类中完成,两种方法各有优缺点,但我认为在子类中操作更好。
另外我尝试过只使用一个布局文件,通过代码只让一部分的View
或Layout
旋转,从而得到更好的相机预览旋转效果,但没有成功。不过从一些典型的相机APP中可以发现,还是可以只使用一个布局文件,但又能够完美处理相机预览旋转的。参考中列出了相关的尝试。
DEMO
本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-OrientationAndRatio。
参考
- Camera | Android Developers
- Camera.Parameters | Android Developers
- MediaRecorder | Android Developers
- ViewGroup | Android Developers
- Layouts | Android Developers
- Styles and Themes | Android Developers
- How to set Android camera orientation properly? - Stack Overflow
- android - Controlling the camera to take pictures in portrait does not rotate the final images - Stack Overflow
- rotation - Is it possible to just rotate some views in screen when rotate my android phone? - Stack Overflow