美文网首页
Android相机开发(四): 旋转与纵横比

Android相机开发(四): 旋转与纵横比

作者: 南北VS东西 | 来源:发表于2023-01-04 10:25 被阅读0次

    转载自:Penguin

    Android Camera Develop: orientation/rotation and aspect ratio

    概述

    接上篇,在实现了相机的基础功能后,着眼于解决预览、拍照与录像时屏幕的旋转,以及预览时的纵横比等问题。上篇实现的相机APP只能以横屏方向预览、拍照和录像,在实际使用时会很不方便;本篇就会实现APP随设备的旋转而旋转,且可以在任意旋转角度上进行预览、拍照和录像。上篇实现的相机APP中,预览画面是充满整个屏幕的,即使调整预览分辨率后,预览画面所占尺寸也不变,这样会造成实际画面被拉长或压扁,十分不友好;本篇会实现预览画面纵横比与分辨率纵横比保持相同,即预览画面不会出现拉长或压扁的情况。

    预览画面随设备旋转

    如果只是简单地将AndroidManifest中横屏锁去掉,虽然可以实现APP随设备旋转,但预览画面不会随之旋转,如下图所示:

    Screenshot Preview Landscape Screenshot 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

    <?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,此时在横屏下还是之前的布局,但在竖屏下已经应用新的布局了,如下图所示

    Screenshot Preview Portrait New Layout

    拍照与录像随预览旋转

    现在预览和界面没问题了,但如果你拍张照,或者录像的话会发现照片或视频的方向没有随着预览方向而改变,也就是说不是“所见即所得”,现在来着手解决这个问题。

    这个问题不是bug,而是Android故意的,我们也是需要手动指定拍照和录像的旋转角度。

    修改拍照旋转角度

    照片的旋转角度是在CameraParameters中指定的。这里有一个很困惑的地方,我们之前设置的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

    <style name="PreferenceTheme">
        <item name="android:textColor">#FFF</item>
        <item name="android:textColorSecondary">#FFF</item>
    </style>
    

    创建一个名为PreferenceTheme的主题,主题只是修改文本颜色为纯白。

    SettingsFragmentonCreate()中,addPreferencesFromResource()之后加入

    XML

    getActivity().setTheme(R.style.PreferenceTheme);
    

    即应用此主题。

    这样偏好设置文本颜色就变为白色了,容易区分多了吧!

    一点唠叨

    本篇完成后,这个相机APP就离实用的APP更近了一步,甚至已经满足了大多数的需求。其实在旋转与纵横比的实现上,需要仔细研究诸如继承关系、生命周期等的问题,但本文没有谈及这些内容,还望想要了解其中细节的读者自己去研究。在纵横比的实现中,我采用的是在子类中获取父类,整个过程在子类中完成;但典型的方法是在父类中获取子类,整个过程在父类中完成,两种方法各有优缺点,但我认为在子类中操作更好。

    另外我尝试过只使用一个布局文件,通过代码只让一部分的ViewLayout旋转,从而得到更好的相机预览旋转效果,但没有成功。不过从一些典型的相机APP中可以发现,还是可以只使用一个布局文件,但又能够完美处理相机预览旋转的。参考中列出了相关的尝试。

    DEMO

    本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-OrientationAndRatio

    参考

    相关文章

      网友评论

          本文标题:Android相机开发(四): 旋转与纵横比

          本文链接:https://www.haomeiwen.com/subject/niulcdtx.html