安卓基础知识——Camera的使用详解(附demo)

一、前言

一般APP开发或多或少会涉及到相机相关功能,对应一般的功能,调用系统的拍摄功能能满足要求,但是如果需要自定义UI,或希望在本APP内完成,这就需要了解Camera的使用了。
本篇将先介绍Camera相关的知识点,然后结合单例例子总结如何自定义Camera,最后梳理Camera开发需要注意的问题。
通过本篇可以了解一下知识点:
1、什么是SurfaceView,有什么作用?何为双缓冲机制?
2、Camera与Camera2对比,如何选择?
3、相机涉及的方向的概念,如何旋转到正确的方向?
4、Camera常用的方法及属性有哪些?
5、Camera的调用流程?
6、需要哪些权限,怎样动态申请?
7、如何设置参数,适配预览区域大小?
8、什么时机打开和关闭摄像头?
9、如何切换前后摄像头?

二、理解SurfaceView

1、为什么需要SurfaceView

安卓系统设定的刷新频率是60FPS(Frame Per Second),即每隔16.6ms底层会发出VSYNC信号重绘界面。如果绘制过于复杂,无法保证60FPS,则会出现卡顿现象。View,ViewGroup,Animator的代码执行全部是在主线程中完成的,如果执行复杂逻辑,轻则容易出现卡顿,严重则可能导致ANR。为了解决这个问题,引入了SurfaceView,主要用于游戏、视频等视觉效果复杂、刷新频率高的场景。
SurfaceView的改进在于引入了双缓冲机制,及多线程绘制。

2、双缓冲机制

如果不用画布,直接在窗口上绘制叫无缓冲绘图;
如果只有一个画布,先将所有内容绘制到画布上,后一次性绘制到窗口叫单缓冲绘图,画布是一层缓冲区;
如果用2个画布,先在一个缓冲画布上绘制所有图像,绘制好后将内容拷贝到正式绘制的画布上,这是双缓冲机制,拷贝比直接绘制效率要高。
在SurfaceView中,一般会开启一个新线程,然后在新线程的中通过SurfaceHolder的lockCanvas方法获取到Canvas进行绘制操作,绘制完以后再通过SurfaceHolder的unlockCanvasAndPost方法释放canvas并提交更改,下次刷新显示新内容。

3、SurfaceView、SurfaceHolder、Surface三者关系

典型的MVC关系:
Surface:Model层,持有缓冲画布Canvas和绘图内容相关的各种信息;
SurfaceView:View层,与用户交互,负责将Surface的内容展示给用户;
SurfaceHolder:Controller层,通过SurfaceHolder控制Surface中的数据。

三、Camera开发相关知识

1、Camera与Camera2

Google从android 5.0开始推出的一套新相机接口Camera2,并摒弃了旧的接口Camera,从Camera到Camera2整套相机框架都发生了变化,所以接口有很大的不同,Camera2解决了Camera寥寥无几的接口和灵活性低的问题,给应用层提供了更多控制权限,以构建更高质量的相机应用。Camera2有很多Camera不支持的特性,如更加先进的框架,可以控制每一预览帧的参数,高速连拍,调整focus distance,控制曝光时间等。
不过很多应用要支持5.0以下设备,所以很多时候依然使用Camera开发相机应用。
本篇主要介绍Camera的应用。
Camera2的应用可以参考Camera2 系列教程《Android Camera2 教程 · 第一章 · 概览》

2、相机方向

相机方向比较难理解,容易搞混,所以这里专门介绍。
首先理解几个方向的概念。
自然方向
人自然站立的方向。

设备方向
设备方向是指设备方向与自然方向的顺时针夹角,例如手机竖着拿正对屏幕是手机的自然方向,即设备方向为0°,如果手机横着拿正对屏幕且顶部在右边,则设备方向为90°,以此类推,而平板横着拿正对屏幕是平板的自然方向,即0°。
获取方向可以通过
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
也可以通过OrientationEventListener 监听旋转。

摄像头方向
手机Camera的图像数据都是来自于摄像头硬件的图像传感器(Image Sensor),摄像头的方向取决于图像传感器的安装方向。安装之后,有一个默认的取景方向,且不会被改变。但为什么手机旋转预览画面也能跟着旋转到正确的画面(自然方向)?是因为Android系统底层根据当前手机屏幕的方向对图像Sensor采集到的数据进行了旋转处理,然后后才送给显示系统。
手机后置摄像头一般是横屏安装的,
CameraInfo.orientation表示相机图像的方向。它的值是相机图像顺时针旋转到设备自然方向一致时的角度。

注意:不同机型的摄像头方向可能不一致,并不是所有都是 90°,也有小部分是 0° 的,所以我们要通过 Camera.CameraInfo.orientation 去判断方向,而不是假设所有设备的摄像头传感器方向都是 90°。

摄像头方向.png

预览方向
系统提供了接口设置预览方向,setDisplayOrientation,默认情况是0°,即预览方向与摄像头方向一致,对于横屏应用,不需要设置预览方向。而对于竖屏应用,则需要通过该接口将Camera的预览方向旋转90,与手机屏幕方向一致,这样才会得到正确的预览画面。

预览方向.png

拍摄方向
相机采集图像后需要进行顺时针旋转的角度,即相机属性的orientation的值。当点击拍照时,得到的照片方向不一定与预览方向一致,因为通过setDisplayOrientation仅仅修改了预览图像的方向,不会影响到实际拍摄图像的方向,需要修改拍摄图像方向可以通过camera.setRotation实现。

拍摄方向.png

3、适配预览区域大小

一般手机会提供多个预览和拍照尺寸,通过接口getSupportedPreviewSizes和getSupportedPictureSizes可以获得这些尺寸列表。如果previewSize比例与预览区(SurfaceView)比例不一致,则看到的预览图像会变形拉伸。如何适配不同预览区大小解决拉伸问题,一般有2种方案:
一是根据previewSize比例修改SurfaceView的比例,调整预览区比例(只调整宽或高)为预览尺寸比例,从而使图像不发生变形。
二是根据SurfaceView大小固定,然后根据其比例选择最佳的(比例最接近的)预览尺寸。

4、YUV/NV21

通过相机预览拿到的图像帧默认是NV21格式的byte数组,Google支持的Camera Preview Callback的YUV常用格式有两种:NV21 / YV12,至于YUV的理解可以参考YUV。而NV21格式数据需要经过特定转化才能转化为BitMap:

public Bitmap nv21ToBitmap(byte[] data){
      try{
             YuvImage image = new YuvImage(data, ImageFormat.NV21, w, h, null);
             ByteArrayOutputStream os = new ByteArrayOutputStream(mData.length);
             if(!image.compressToJpeg(new Rect(0, 0, w, h), 100, os)){
                   return null;
             }
             byte[] tmp = os.toByteArray();
             os.close();
             return BitmapFactory.decodeByteArray(tmp, 0,tmp.length); 
      }catch(Exception e){
      }
      return null;
}

四、 Camera方法及内部类介绍

1、常用方法介绍

getNumberOfCameras():获取摄像头个数
getCameraInfo(int cameraId, Camera.CameraInfo cameraInfo):获取相机信息
open(int cameraId):打开相机
setPreviewDisplay(SurfaceHolder holder):设置预览方向
startPreview():开始预览
stopPreview():停止预览
setPreviewCallback(Camera.PreviewCallback cb):设置预览回调,callback中可以得到二进制预览帧
setPreviewCallbackWithBuffer(Camera.PreviewCallback cb):setPreviewCallback每产生一帧都开辟一个新的buffer,会导致频繁GC,影响效率,如果对效率要求比较高,则用setPreviewCallbackWithBuffer,指定缓冲区,通过内存复用提高效率
autoFocus(Camera.AutoFocusCallback cb):自动聚焦
takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):拍摄照片
setParameters(Camera.Parameters params):设置相机参数
Camera.Parameters getParameters():获取相机参数

2、内部类

CameraInfo
facing:摄像头的方向,包括前置和后置方向,可选值有 Camera.CameraInfo.CAMERA_FACING_BACK 和Camera.CameraInfo.CAMERA_FACING_FRONT。
orientation:摄像头画面经过多少度旋转可变为自然方向。

Parameters
通过LinkedHashMap<String, String>存储相机的参数。
get(String key):从map中读取指定key的参数
setPreviewSize(int width, int height):设置预览尺寸
Camera.Size getPreviewSize():获取预览尺寸
List<Size> getSupportedPreviewSizes:获取相机支持的预览尺寸,不同机型支持的参数不同
List<Size> getSupportedVideoSizes():获取录制视频的尺寸,不同机型支持的参数不同
setPictureSize(int width, int height):设置拍摄图像尺寸,是拍摄后图像,不是预览
List<Size> getSupportedPictureSizes():获取相机支持的拍摄尺寸,不同机型支持的参数不同
setRotation(int rotation):设置拍摄返回图像的方向,不是预览方向

六、相机调用流程

因为相机设计的接口比较多,为了职责分明,将相机相关的逻辑封装到CameraManager类,相机参数设置封装到CameraConfigurationManager类,这里直接参考Zxing开源代码中的Camera部分,并稍作修改。

1、Acitivty部分

注册权限

因为拍摄照片要保存到本地,所以除了相机权限还需要存储权限。

 <!-- 操作本地文件 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!-- 摄像头使用 -->
    <uses-permission android:name="android.permission.CAMERA"/>

动态申请权限

对于target sdk 为23(安卓6.0)以上的应用,运行中6.0以上系统,需要动态申请权限。在打开相机前先判断系统版本,如果大于6.0则判断是否已经申请权限,没有则申请。申请权限如果客户拒绝了则退出拍摄页面(也可以在跳着拍摄页面前申请,不过需要在所有打开拍摄页面的地方都申请),如果客户选择了不再提示,则弹出提示框,引导客户到系统设置中开启权限,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           String[] deniList = checkPermissionsGranted(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA});
           if (deniList != null && deniList.length > 0) { //未授权
               requestPermissions(deniList, REQ_PERMISSION);
           } else {
               openCamera();
           }
       } else {
           openCamera();
       }
    @TargetApi(23)
    public String[] checkPermissionsGranted(String[] permissions) {
        List<String> deniList = new ArrayList<>();

        // 遍历每一个申请的权限,把没有通过的权限放在集合中
        for (String permission : permissions) {
            if (checkSelfPermission(permission) !=
                    PackageManager.PERMISSION_GRANTED) {
                deniList.add(permission);
            }
        }
        return deniList.toArray(new String[deniList.size()]);
    }
 @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults.length > 0) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                openCamera();
            } else {
                boolean camera = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA);
                if (!camera) { // 判断是否勾选了不再提醒,如果有勾选,提权限用途,点击确定跳转到App设置页面
                    AlertDialog.Builder builder = new AlertDialog.Builder(this);
                    mDialog = builder.setMessage("请在设置-应用-xxx中,设置运行使用摄像头权限").setPositiveButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            finish();
                        }
                    }).setNegativeButton("去设置", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            goIntentSetting();
                        }
                    }).create();
                    mDialog.setCancelable(false);
                    mDialog.show();
                } else { //拒绝了权限
                    finish();
                }
            }
        }
    }

    /**
     * 应用设置页面
     */
    private void goIntentSetting() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", getPackageName(), null);
        intent.setData(uri);
        try {
            intent.addCategory(Intent.CATEGORY_DEFAULT);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

布局定义SurfaceView并设置Callback

protected void onCreate(@Nullable Bundle savedInstanceState) {
       ...
       mSurfaceView = findViewById(R.id.surfaceView);
       mSurfaceView.getHolder().addCallback(this);
       ...
}

  @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

打开相机并预览

在surfaceCreated回调中,执行判断权限,确保有权限后再打开相机,打开相机加try-catch以防遇到异常程序crash,遇到异常则弹出提示。

   private void openCamera() {
        try {
            //设置前置或后置摄像头
            mCameraManager.setManualCameraId(mIsFront ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK);
            //打开摄像头
            mCameraManager.openDriver(mSurfaceView.getHolder());
            //开始预览
            mCameraManager.startPreview();
        } catch (Exception ioe) {
            //捕获异常,提示并退出
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            mDialog = builder.setMessage("打开摄像头失败,请退出重试").setNegativeButton("确定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    finish();
                }
            }).create();
            mDialog.setCancelable(false);
            mDialog.show();
        }
    }

拍照

mShoot = findViewById(R.id.shoot);//拍摄按钮,点击拍摄
mShoot.setOnClickListener(new View.OnClickListener() {
      @Override
       public void onClick(View view) {
                mCameraManager.getCamera().takePicture(null, null, new Camera.PictureCallback() {
                @Override
                 public void onPictureTaken(byte[] bytes, Camera camera) {
                                       Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                        Camera.CameraInfo info = mCameraManager.getCameraInfo();
                        bitmap = BitmapUtil.rotateAndMirrorBitmap(bitmap, info.orientation, info.facing == 
                        Camera.CameraInfo.CAMERA_FACING_FRONT);
                        mImgResult.setImageBitmap(bitmap);//展示照片
                        mImgResult.setVisibility(View.VISIBLE);
                        mLayoutOpe.setVisibility(View.VISIBLE);
                        mShoot.setVisibility(View.GONE);
                        mImgChange.setVisibility(View.GONE);
                        mSurfaceView.setVisibility(View.GONE);
                        mCameraManager.stopPreview();//停止预览
                    }
                });
            }
        });

切换前后摄像头

每次切换摄像头都需要先将当前摄像头关闭,然后设置并开启新的摄像头。

//点击切换按钮
 mImgChange.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mCameraManager.closeDriver();//关闭当前摄像头
                mIsFront = !mIsFront;
                openCamera();//开启新摄像头
            }
        });

关闭摄像头

每次打开摄像头后都需要主动关闭摄像头,不然摄像头没有解锁,下次不能正常打开。这里在surfaceDestroyed回调中关闭,也可以在activity的onpause方法执行,当打开新的页面、退出页面或者藏后台都会调用关闭。

  @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mCameraManager.closeDriver();
    }

2、CameraManager类

打开摄像头

    public synchronized void openDriver(SurfaceHolder holder)
            throws IOException {
        Camera theCamera = camera;

        if (theCamera == null) {
            //打开摄像头
            if (requestedCameraId >= 0) {
                theCamera = OpenCameraInterface.open(requestedCameraId);
            } else {
                theCamera = OpenCameraInterface.open();
            }

            if (theCamera == null) {
                throw new IOException();
            }
            camera = theCamera;
        }
        //设置SurfaceHolder
        theCamera.setPreviewDisplay(holder);

        if (!initialized) {
            initialized = true;
            //初始化相机参数,根据surfaceview大小判断最近预览尺寸(只是判断,没有实际设置)
            configManager.initFromCameraParameters(theCamera, holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
            if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
                setManualFramingRect(requestedFramingRectWidth,
                        requestedFramingRectHeight);
                requestedFramingRectWidth = 0;
                requestedFramingRectHeight = 0;
            }
        }

        Camera.Parameters parameters = theCamera.getParameters();
        String parametersFlattened = parameters == null ? null : parameters
                .flatten(); // Save these, temporarily
        try {
            //设置相机参数,预览方向
            configManager.setDesiredCameraParameters(theCamera, requestedCameraId);
        } catch (RuntimeException re) {
            // Driver failed
            Log.w(TAG,
                    "Camera rejected parameters. Setting only minimal safe-mode parameters");
            Log.i(TAG, "Resetting to saved camera params: "
                    + parametersFlattened);
            // Reset:
            if (parametersFlattened != null) {
                parameters = theCamera.getParameters();
                parameters.unflatten(parametersFlattened);
                try {
                    theCamera.setParameters(parameters);
                    configManager.setDesiredCameraParameters(theCamera, requestedCameraId);
                } catch (RuntimeException re2) {
                    // Well, darn. Give up
                    Log.w(TAG,
                            "Camera rejected even safe-mode parameters! No configuration");
                }
            }
        }
    }

开始、停止预览

    /**
     * Asks the camera hardware to begin drawing preview frames to the screen.
     */
    public synchronized void startPreview() {
        Camera theCamera = camera;
        if (theCamera != null && !previewing) {
            theCamera.startPreview();
            previewing = true;
            autoFocusManager = new AutoFocusManager(camera);
        }
    }

    /**
     * Tells the camera to stop drawing preview frames.
     */
    public synchronized void stopPreview() {
        if (autoFocusManager != null) {
            autoFocusManager.stop();
            autoFocusManager = null;
        }
        if (camera != null && previewing) {
            camera.stopPreview();
            previewCallback.setHandler(null, 0);
            previewing = false;
        }
    }

3、CameraConfigurationManager类

判断相机最佳预览尺寸

从支持的尺寸列表中选出与预览界面大小比例最接近的

 public void initFromCameraParameters(Camera camera, int width, int height) {
        Camera.Parameters parameters = camera.getParameters();

        WindowManager manager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
        Display display = manager.getDefaultDisplay();
        screenResolution = new Point(display.getWidth(), display.getHeight());
        Log.d(TAG, "Screen resolution: " + screenResolution);

        Point screenResolutionForCamera = new Point();
        if (width == 0 || height == 0) {
            screenResolutionForCamera.x = screenResolution.x;
            screenResolutionForCamera.y = screenResolution.y;
        } else {
            screenResolutionForCamera.x = width;
            screenResolutionForCamera.y = height;
        }

        if (screenResolution.x < screenResolution.y) {
            screenResolutionForCamera.x = screenResolution.y;
            screenResolutionForCamera.y = screenResolution.x;
        }

        previewResolution = getPreviewResolution(parameters, screenResolutionForCamera);
        pictureResolution = getPictureResolution(parameters, screenResolutionForCamera);
 }

获取最近预览尺寸:

private static Point findBestSizeValue(CharSequence sizeValueString, Point screenResolution) {
        int bestX = 0;
        int bestY = 0;
        int diff = Integer.MAX_VALUE;
        for (String previewSize : COMMA_PATTERN.split(sizeValueString)) {

            previewSize = previewSize.trim();
            int dimPosition = previewSize.indexOf('x');
            if (dimPosition < 0) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newX;
            int newY;
            try {
                newX = Integer.parseInt(previewSize.substring(0, dimPosition));
                newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
            } catch (NumberFormatException nfe) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newDiff = Math.abs(newX - screenResolution.x) + Math.abs(newY - screenResolution.y);
            if (newDiff == 0) {
                bestX = newX;
                bestY = newY;
                break;
            } else if (newDiff < diff) {
                bestX = newX;
                bestY = newY;
                diff = newDiff;
            }

        }

        if (bestX > 0 && bestY > 0) {
            return new Point(bestX, bestY);
        }
        return null;
    }

获取最佳拍摄尺寸:

  private Point getPictureResolution(Camera.Parameters parameters, Point screenSize) {
        String pictureSizeValueString = parameters.get("picture-size-values");
        // saw this on Xperia
        if (pictureSizeValueString == null) {
            pictureSizeValueString = parameters.get("picture-size-value");
        }

        Point pictureSize = null;

        if (pictureSizeValueString != null) {
            pictureSize = findBestSizeValue(pictureSizeValueString, screenSize);
        }

        if (pictureSize == null) {
            // Ensure that the camera resolution is a multiple of 8, as the screen may not be.
            pictureSize = new Point(
                    (screenSize.x >> 3) << 3,
                    (screenSize.y >> 3) << 3);
        }

        return pictureSize;
    }

判断最佳尺寸:比例最接近的

 private static Point findBestSizeValue(CharSequence sizeValueString, Point screenResolution) {
        int bestX = 0;
        int bestY = 0;
        int diff = Integer.MAX_VALUE;
        for (String previewSize : COMMA_PATTERN.split(sizeValueString)) {

            previewSize = previewSize.trim();
            int dimPosition = previewSize.indexOf('x');
            if (dimPosition < 0) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newX;
            int newY;
            try {
                newX = Integer.parseInt(previewSize.substring(0, dimPosition));
                newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
            } catch (NumberFormatException nfe) {
                Log.w(TAG, "Bad preview-size: " + previewSize);
                continue;
            }

            int newDiff = Math.abs(newX - screenResolution.x) + Math.abs(newY - screenResolution.y);
            if (newDiff == 0) {
                bestX = newX;
                bestY = newY;
                break;
            } else if (newDiff < diff) {
                bestX = newX;
                bestY = newY;
                diff = newDiff;
            }

        }

        if (bestX > 0 && bestY > 0) {
            return new Point(bestX, bestY);
        }
        return null;
  }

设置相机参数

设置预览尺寸、拍摄尺寸、焦距、预览方向

  public void setDesiredCameraParameters(Camera camera, int cameraId) {
        Camera.Parameters parameters = camera.getParameters();
        Log.d(TAG, "Setting preview size: " + previewResolution);
        parameters.setPreviewSize(previewResolution.x, previewResolution.y);
        parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
        setZoom(parameters);
        setCameraDisplayOrientation(camera, cameraId);
        camera.setParameters(parameters);
   }

设置预览方向

private void setCameraDisplayOrientation(Camera camera, int cameraId) {
        Camera.CameraInfo info = new Camera.CameraInfo();
        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);
}

旋转照片

对于拍摄照片,一般可以对拍摄后照片根据相机角度(orientation)进行旋转到自然方向,特别地,对于前置摄像头的照片,需要做镜像对换。代码如下:

  public static Bitmap rotateAndMirrorBitmap(Bitmap bm, int degree, boolean needMirror) {
        Bitmap newBm = null;
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        if (needMirror) {
            matrix.postScale(-1, 1); // 镜像水平翻转
        }
        try {
            // 将原始图片按照旋转矩阵进行旋转,并得到新的图片
            newBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(),
                    bm.getHeight(), matrix, true);
        } catch (OutOfMemoryError e) {
        }
        if (newBm == null) {
            newBm = bm;
        }
        if (bm != newBm) {
            bm.recycle();
        }
        return newBm;
    }

七、测试效果

下面通过实验测试各种设置的图像效果,测试机:华为P10。

1、预览方向

如果不设置预览方向:

    public void setDesiredCameraParameters(Camera camera, int cameraId) {
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(previewResolution.x, previewResolution.y);
        parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
        setZoom(parameters);
//        setCameraDisplayOrientation(camera, cameraId);
        camera.setParameters(parameters);
    }

如果Activity可随手机方向旋转的情况:


预览效果(不设置预览方向-后置-界面可旋转).jpg

如果Activity不可随手机方向旋转的情况:


预览效果(不设置预览方向-后置-界面不旋转).jpg

可以看到,如果是横屏应用(Acitivity方向是横向),预览方向是自然方向,不需要旋转,如果是竖屏应用(Acitivity竖直方向),预览图像需要旋转90°。
前置摄像头同样效果。

设置预览方向后

无论Acitivity是否可随手机方向旋转,预览图像都是自然方向。前置摄像头也是自然方向,但图形是镜像图,左右对换的,跟照镜子一样。


预览效果(设置预览方向- 后置-可旋转).jpg

预览效果(设置预览方向-前置-可旋转).jpg

2、预览尺寸

如果不设置预览尺寸:

    public void setDesiredCameraParameters(Camera camera, int cameraId) {
        Camera.Parameters parameters = camera.getParameters();
//        parameters.setPreviewSize(previewResolution.x, previewResolution.y);
        parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
        setZoom(parameters);
        setCameraDisplayOrientation(camera, cameraId);
        camera.setParameters(parameters);
    }

如果预览界面大小是全屏,预览尺寸没有变形,说明相机默认预览尺寸比例与手机宽高比例一致的。
修改一下预览界面大小,测试一下预览图像效果。


预览效果(不设置预览尺寸).jpg

设置最佳预览尺寸后

预览效果(设置预览尺寸).jpg

可以发现,如果不设置最佳预览尺寸,预览图像可能会严重变形,测试中横屏特别明显,变形程度与预览宽高比例与SurfaceView宽高比例差异大小有关。

拍照方向

不设置拍照方向

如果不设置拍照方向,对于后置摄像头,无论界面是否可以旋转的效果:


拍照效果(后置-不设置拍照方向).jpg

可以看到,对于横屏拍摄,拍出的照片都是自然方向的,而对于竖屏拍摄,拍出来的照片需要旋转后90°才是自然方向。(一般手机是旋转90°,但也有少数是270°,如Nexus 5X)。

再看下前置摄像头的效果:


拍摄效果(前置-不设置拍照方向).jpg

界面不随手机旋转的效果同样。
可以看到,横屏拍摄是自然方向,竖屏拍照,照片需要旋转270°才是自然方向(一般手机是旋转270°,但也有少数是90°)

对拍摄照片旋转后

无论前后摄像头,无论横屏还是竖屏,拍照后需要根据相机方向调整照片到自然方向,另外,前置摄像头调整到自然方向发现跟预览图像刚好左右相反的,正常应该跟预览,或者照镜子看到的一致,所以还需要做镜像对换。镜像对换前(左)和镜像对换后(右)的效果如下:


拍摄效果(前置-镜像对换).jpg

八、其他问题

1、预览模糊/拍照照片模糊
设置的previewSize和pictureSize太小,或者没有对焦。
2、预览/拍摄照片已经对焦,但光线很暗
可能跟预览分辨率有关,调整试下。
如测试过程发现,Honor 8C(EMUI 8.2.0,android8.1.0) 这款手机,预览分别是1280*720的时候,预览会很暗。

九、总结

1、SurfaceView相比一般的View改进在于引入了双缓冲机制,多线程绘制,主要用于视频、游戏等视觉复杂、刷新频率高的场景;
2、双缓冲机制简单来说就是使用多个画布,在新线程绘制到缓冲画布上,然后直接拷贝到正式绘制画布上,实现高效绘制显示;
3、Surface、SurfaceView、SurfaceHolder三者是典型的MVC关系;
4、Camera2支持更多的相机特性,但不支持5.0以下手机,为了兼容低版本,很多应用依然使用Camera1;
5、摄像头一般是横屏安装的,决定了取景方向,且不会随设备改变方向。CameraInfo.orientation表示相机图像的方向,表示相机图像顺时针旋转到设备自然方向一致时的角度,一般是90°;
6、camera.setDisplayOrientation可以设置预览方向,但不会影响拍摄照片的方向,横屏应用不需要设置,竖屏需要;
7、camera.setRotation可以设置拍摄照片的方向,另一种方法是根据CameraInfo.orientation对拍摄后的照片进行旋转到自然方向;
8、为了适配不同预览区域大小、解决预览拉伸问题,一种方案是从相机支持的预览尺寸中选出宽高比例最接近预览区域比例的尺寸;另外一种是相机预览尺寸不变,调整预览区域宽或高的一边。
9、预览返回的图像是NV21格式的,需要转换才能变为BitMap。

文中demo:STakePicture

参考

Android自定义View之双缓冲机制和SurfaceView
Android: Camera相机开发详解(上) —— 知识储备

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容