Android Activity&Camera旋转方向分析详解

1. 前言

安卓开发中经常有需要使用摄像头的应用场景,初次接触的话屏幕方向和摄像头的方向是个比较难弄清楚的概念,开发时很容易处理不当,本文将详述该部分内容帮助理解。

2. Android 的屏幕方向

2.1 什么是屏幕方向

屏幕方向:屏幕方向是对 Activity 而言的,也是 Activity 的方向。Activity 是 Android 组件中最基本也是最为常见用的四大组件之一。它也是一个界面的载体,在 Android 中代表了界面和以界面为中心相应的业务逻辑,包括显示、与用户交互等。
屏幕方向的表现形式如下:

2.2 为什么要获取或设置屏幕方向

根据我们的项目开发需求,我们需要获取或设置屏幕方向。在开发中旋转Android 屏幕的方向会影响到很多东西,影响最大的是 Android 页面的布局,接下来我们以页面布局为例子。
需求一:我们希望屏幕方向改变之后,设备的布局也会有相应的调整。比如说我们使用的UC浏览器,我们切换手机的方向,它的页面会自动旋转。
需求二:无论设备怎么旋转,设备的屏幕方向一直不变。比如最近很火的吃鸡游戏,我们为了增加游戏体验,需要锁定的屏幕为横屏方向。
这两种情况正好对应了为什么要获取屏幕方向和为什么要设置屏幕方向。

2.3 如何获取与设置屏幕方向
针对于软件开发的需求情况,我们有时候只需要获取屏幕的方向亦是只需要设置屏幕的方向。
下面我将用代码介绍我们的这两种方式 。

2.3.1 如何获取屏幕方向

获取 Android 的屏幕方向一共有两种方法。(1)横竖屏方向的求取。(2)屏幕切换角度值的求取。屏幕切换角度值:即我们将设备垂直放置,设备旋转了多少角度。

(1)横竖屏方向的求取:

/*** 判断Android横竖屏**/Configuration mConfiguration = this.getResources().getConfiguration(); //获取设置的配置信息int orientation = mConfiguration.orientation ; //获取屏幕方向if (orientation == Configuration.ORIENTATION_PORTRAIT) {//竖屏else if (orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {//横屏}

注意:判断Android横竖屏我们可以用肉眼一看就分辨出来,但是程序不行,因此我们需要用上述代码来实现。

(2)屏幕切换角度值的求取

/*** 求取屏幕切换角度值
**/
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();int degrees = 0;switch (rotation) {case Surface.ROTATION_0: degrees = 0break;case Surface.ROTATION_90: degrees = 90break;case Surface.ROTATION_180: degrees = 180break;case Surface.ROTATION_270: degrees = 270break;}

代码中switch里面的case分别与屏幕切换角度值是一一对应的。

2.3.2 如何设置屏幕方向

设置 Android 的屏幕方向也是一共有两种方法。(1)AndroidManifest.xml 文件设置。(2)java代码中设置。

(1)AndroidManifest.xml 文件设置:

为了保证该 Activity 横屏运行,横屏代码设置如下所示:

/*** 添加android:screenOrientation属性
**/<activityandroid:name=".MainActivity"android:screenOrientation="landscape"></activity>

如果将屏幕竖屏设置,只需要将android:screenOrientation="landscape"中的landscape替换成portrait。通过这种xml设置,我们的界面一打开就加载MainActivity就是横屏设置。而且无论手机怎么旋转,这个MainActivity始终保持水平的方向。

(2)java代码中设置:

java代码中也可以设置屏幕的指定方向,代码如下:

/*** 将MainActivity屏幕设置为横屏**/

public class MainActivity extends Activity { 

@Overrideprotected void onCreate(Bundle savedInstanceState) { 

super.onCreate(savedInstanceState);

//代码设置横竖屏(landscape:横屏---portrait:竖屏)

setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

上述代码是在MainActivity中的onCreate方法中添加setRequestedOrientation属性,完成屏幕横屏的显示。
设置横屏代码:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
设置竖屏代码:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

3. Android 的摄像头方向

3.1 什么是摄像头方向

摄像头方向:摄像头的方向与图像传感器的方向有关,这个 Sensor 被固定到手机之后有一个默认的取景方向,与手机正常方向相差一定的角度。
通俗一点讲,我们将设备当作人的身体,眼睛当作摄像头。眼睛把接收到的画面反馈给大脑处理,相当于摄像头把接收到的数据交给应用程序处理。人眼能判断出我们头顶向上的方向是我们视觉上的正向,而后置摄像头判断的正向并不是手机物理屏幕向上的方向,而是物理屏幕右侧的方向。我们想象一下,如果人眼是这个摄像头,它认为右侧才是我们的视觉正向,那我们看到的东西都是旋转90度的。参考图如下:

官方文档对摄像头方向的解释:

The orientation of the camera image. The value is the angle that the camera image needs to be rotated clockwise so it shows correctly on the display in its natural orientation. It should be 0, 90, 180, or 270.
For example, suppose a device has a naturally tall screen. The back-facing camera sensor is mounted in landscape. You are looking at the screen. If the top side of the camera sensor is aligned with the right edge of the screen in natural orientation, the value should be 90. If the top side of a front-facing camera sensor is aligned with the right of the screen, the value should be 270.

注意:正向始终在物理屏幕的右侧,orientation 就等于从摄像头的角度看,从物理设备的正上方向,需要顺时针旋转多少度才能到正向的箭头。所以,根据这个想象一下前后摄像头的区别,这个值后置摄像头是90,前置摄像头是270。

3.2 为什么要获取摄像头方向

比如当我们拍照的时候,在手机页面上显示的图像和不经过任何处理输出的原始图像会有很大区别,如下图所示:

使用Camera 录制一个视频, 然后使用MediaMetadataRetriever来解析, 可以获取视频的orientation信息

MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(path2video);
mVideoOrientation = retriever.extractMetadata(METADATA_KEY_VIDEO_ROTATION);
mVideoDuration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
mVideoWidth = retriever.extractMetadata(METADATA_KEY_VIDEO_WIDTH);
mVideoHeight = retriever.extractMetadata(METADATA_KEY_VIDEO_HEIGHT);
Log.d(TAG, String.format("Retrieve video info %sx%s, orientation: %s, duration:%s", mVideoWidth, mVideoHeight, mVideoOrientation, mVideoDuration));

//portrait
Retrieve video info 1920x1080, orientation: 90, duration:1659
//landscape
Retrieve video info 1920x1080, orientation: 0, duration:8042

为了解决旋转角度这一问题,我们需要将摄像头的流数据无旋转角度的显示在 Camera 屏幕中,因此我们首先应该获取摄像头方向。

3.3 如何获取摄像头方向

代码如下所示:

/*** API 获取摄像头方向**/

public void getCameraOrientation() {

android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();

int cameraOrientation = info.orientation;

}
API1 获取Camera Sensor 角度

通过这个方法,就能得到图像传感器的默认的取景方向,也就是摄像头的方向。

4. Android Camera界面显示预览

我们的目的其实是为了在Camera界面正确显示预览。 因为 Camera 默认的方向是横的,相对手机的自然方向逆时针旋转了90度。也就是说如果你的应用是竖屏应用,就必须通过一个方法将 Camera 的预览方向旋转 90 度,让摄像头预览方向与手机屏幕方向保持一致,这样才会得到正确的预览画面。

4.1 Camera 预览的总体流程

要实现Camera界面的显示预览,我们的两个角度即屏幕切换角度值和摄像头的方向一定要先获取到。然后我们通过这两个角度计算出屏幕的显示方向,具体步骤如下:
1.获取屏幕切换角度值。
2.获取摄像头方向。
3.设置相机显示方向。

4.2 Camera 预览界面代码的流程

流程图示例如下, 注意Sensor 不管设备如何旋转,Sensor 送过来的图都是横图(旋转过后, 还是要转回来的), 如图左一列,都会是Sensor认为的正向。

上图中的第一列是输出图像在流中的方向,默认都是垂直向上。第二列是摄像头物理方向的真实方向。我们为了在设备屏幕中保持合适的方向,我们需要将输出图像的方向与后置摄像头方向保持一致。
第一行的图像中输出图像的方向垂直向上,后置摄像头的方向向右,所以需要将图像顺时针旋转90度。
第二行的图像中输出图像的方向垂直向上,后置摄像头的方向也是向上,它们的方向保持一致,所以我们不需要做任何处理。
第三行的图像中输出图像的方向垂直向上,后置摄像头的方向向左,所以需要将图像顺时针旋转270度。
第一行的图像中输出图像的方向垂直向上,后置摄像头的方向向右,所以需要将图像顺时针旋转180度。

总结:

屏幕显示旋转方向 = 输出画面的方向 - 后置摄像头方向 。
设置屏幕的显示方向代码如下:

/*** 设置相机显示方向的详细解读**/
public static void setCameraDisplayOrientation(Activity activity,int cameraId, android.hardware.Camera camera) {
// 1.获取屏幕切换角度值。
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;
}

// 2.获取摄像头方向。

android.hardware.Camera.CameraInfo info =new android.hardware.Camera.CameraInfo();

android.hardware.Camera.getCameraInfo(cameraId, info);

// 3.设置相机显示方向。

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);

}

前2个步骤理解起来比较容易,但是第3个步骤理解起来计较困难 。它是通过计算屏幕切换角度值和摄像头的方向,重新定位出屏幕的显示方向 。

5. 使用GL Render Camera Preview Data

5.1 数据流

     Camera Hal 会提供sensor rotation, 然后camera fw 会透过ANativeWindow_setBuffersTransform 将transform 设置给Surface(一般情况会是SurfaceView, 区别我们demo apk), 然后SurfaceView 就可以在将buffer 丢给SurfaceFlinger。 SurfaceFlinger根据display rotation和Surface 的transform做最后的合成(rotation)并overlay。 注意这里也就是前面提到的,在Camera sensor的buffer 丢到surfaceflinger前, 都是不存在rotation的, 实际上preview 的rotation (transform)只是个attribute 设置给Surface,最后影响display。

如果将Sensor的frame byte data获取, 自己处理显示, 则相当于丢掉了transform信息, 需要自己handle preview orientation。一个可行方法就是使用OES Texutre 实例化SurfaceTexture,然后丢给Camera, Camera 出图后, 数据会透过SurfaceTexture 上传到OES。然后我们再透过OES render成普通2d 纹理, 注意这个过程实际上并未有handle transfrom(虽然我们可以透过surfaceTexture 拿到transform matrix,但是貌似我们没有这么做), 就需要自己去设定render到另外一个surface(surfaceview for preview)的orientation, 目前我们应该是都是按照90读的rotatin去处理的。

com.anc.human.ui.video.CameraSurfaceRenderer#onDrawFrame


//mSurfaceTexture.getTransformMatrix(mSTMatrix);//这个是从SurfaceTexture去拿CameraService 设定的Transform Matrix, 不过这部分被注掉了
//mtx 这个矩阵是单位矩阵identity matrix  , 相当于不做任何仿射变化, 但是我们传入了camera id(依次判断是否是前置)

mFullScreen.drawFrame(getMatchedTextureID(bRealSeg && segResult), mtx,false, mCameraManager.getCameraId());
/*** Draws a viewport-filling rect, texturing it with the specified texture object.*/
public void drawFrame(int textureId, float[] texMatrix,boolean isTextureIn, int isFront) {
// Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport.
if(isTextureIn)
mProgram.draw(GlUtil.IDENTITY_MATRIX, Drawable2d.FULL_RECTANGLE_BUF, 0,mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(),mRectDrawable.getVertexStride(),texMatrix, mRectDrawable.getTexCoordArray(), textureId,mRectDrawable.getTexCoordStride());
else if(isFront == 1 || isFront == 3 || isFront == 100){
/*
mProgram.draw(GlUtil.IDENTITY_MATRIX, mRectDrawable.getVertexArray(), 0,mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(),mRectDrawable.getVertexStride(),texMatrix, mRectDrawable.getTexCoordArray(), textureId,mRectDrawable.getTexCoordStride());
*/
 mProgram.draw(GlUtil.IDENTITY_MATRIX, Drawable2d.FULL_RECTANGLE_BUF_liao90, 0,mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(),mRectDrawable.getVertexStride(),texMatrix, mRectDrawable.getTexCoordArray(), textureId,mRectDrawable.getTexCoordStride());}else if(isFront == 0 || isFront == 2){
 mProgram.draw(GlUtil.IDENTITY_MATRIX, Drawable2d.FULL_RECTANGLE_BUF_liao90back, 0,mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(),mRectDrawable.getVertexStride(),texMatrix, mRectDrawable.getTexCoordArray(), textureId,mRectDrawable.getTexCoordStride());
}
}

PS:因为上面SurfaceRender 的onDrawFrame会根据当前是否有成功bokeh,来选择对应的render texture,如果需要特别处理矩阵, 需要一并考虑

com.anc.human.ui.video.CameraSurfaceRenderer#renderBuffer

if(isInputTexture2D) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mInputTexure2D,0);
GLES20.glViewport(0,0, mCameraManager.cameraWidth, mCameraManager.cameraHeight);mOESTex22DTex.drawFrame(mTextureId, mtx, true, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
inputTex = mInputTexure2D;is_oes = false;
}

5.2 旋转矩阵

快速便捷的处理Camera 角度适配问题, 可以直接修改哪个mtx(4x4) 转换矩阵

android.opengl.Matrix (4x4)

android.graphics.Matrix (3x3)

齐次坐标, 需要4x4 (x,y, z, 1)

float mtx[] = {1.0f,0.0f,0.0f,0.0f, 0.0f,1.0f,0.0f,0.0f, 0.0f,0.0f,1.0f,0.0f, 0.0f,0.0f,0.0f,1.0f};
if(mCameraManager.getCameraId() == 0){
Matrix.rotateM(mtx, 0, 180, 0, 0, 1);
} else {
Matrix.setIdentityM(mtx, 0);
}
mFullScreen.drawFrame(getMatchedTextureID(bRealSeg && segResult), mtx,false, mCameraManager.getCameraId());

5.3 适配

  1. 快捷修改, 如5.2, 直接添加旋转矩阵(注意一并修改videorecording的mtx)
  2. 正确处理,还是需要根据CameraService 设置的transform 转移到preview的Surface或是直接根据Camera orientation及display orientation来设定

5.4 案例分析

Camera Hal 在启动的时候, 会枚举camera device, 一般情况下, camera id 0 (logical id)会对应到后置, 1(logical id)会对应前设-适应camera api1/2, 基于这个原因APP 将判断是否前置不是透过camera api 不同level的判断方法, 而是直接透过Camera Id来判断~

问题出现在后置(原本camera id0)的sensor在hal 枚举的时候, 没有能成功注册, 就会导致camera id 0 变成了前置。 按照camera id 判断的rule 就会失效。

建议修改是直接透过不同camera api的facing 判断api来获取camera的facing方向, 而不是透过camera id来判断。

6. 总结

以上就是我对安卓的屏幕方向和摄像头方向的理解,有些不足之处希望大家批评指正!

7. 参考资料

 

标签:,

About: kiah