详记Android打开相机拍照流程

写在前面

本文并不是基于Camera2的,所以想要了解Camera2的同学可以先散了。文题加了详记二字,因为相机整个打开的流程的确是比较复杂的,稍有疏忽可能就会引发一系列问题。我也是看了一下Android的文档才整理了这篇文章,想看原文的戳这。不得不说,文档还是详细啊~

本文主要会涉及以下内容:

  • 相机的使用流程
  • 拍照及拍照期间的聚焦
  • 保存图片

先放一下最终效果图吧,做的比较简单,各位不用担心:

最终也就这样~

主要功能就是拍照保存,多的也没啥了,项目地址在文末有。

使用流程

在详细的研究相机之前,首先熟悉一下使用相机的整个流程:

  • 检测和访问相机:创建代码检测相机的存在和请求访问
  • 创建预览类:创建继承自SurfaceView和实现SurfaceHolder接口的预览类。这个类展示来自相机的实时图片
  • 创建一个预览布局:如果你有相机预览类,你就需要创建一个和用户交互的界面布局
  • 为拍照或者录像设置监听:对用户的行为作出响应,你要为你的控件设置监听去开始拍照或者录像,比如你设置了一个拍照的按钮,用户点击之后就要开始拍照。监听用户行为只是其一,还有就是拍照的监听,这个放到后文讨论
  • 捕获和保存文件:无论是拍照还是录像,都需要有保存的功能
  • 释放相机:在不使用相机时候,你的应用一定要释放相机。

那么为什么一定要释放相机资源呢?因为相机硬件是一个共享临界资源,不仅你的应用会使用,其他的应用也会使用相机。所以在不用相机的时候,一定要释放相机,不然你自己的应用和后续其他要使用相机的应用都无法使用相机。

流程大致就是这样,接下来一步一步的跟进,看看这个相机到底特么的是怎么用的。

申请权限

Android 6.0之前 & targetSdkVersion < 23 只需要在清单文件中声明一下权限就行



上面那个uses-feature,文档中说如果声明了这个,在Google Play中会阻止没有相机的设备下载你的应用。国内的应用商店就不知道了= =。在Android 6.0之后且targetSdkVersion >= 23就需要申请相机权限了。我们可以在Activity的onResume中检测是否拥有相机权限,如果拥有权限就进行下一步的操作,如果没有就申请权限,并在回调中检测是否申请成功,成功的话就进行下一步操作。代码如下:

    @Override
    protected void onResume() {
        super.onResume();
        // 检查权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
                PackageManager.PERMISSION_GRANTED) {
            // 申请权限
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1);
        } else {
            // 已有权限
            startCameraPre();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 如果权限申请成功
            startCameraPre();
        } else {
            Toast.makeText(this, "您已拒绝打开相机,想要使用此功能请手动打开相机权限", Toast.LENGTH_SHORT).show();
        }
    }

检测相机是否存在

在使用之前需要先检测一下设备是否有相机:

    /**
     * 检查是否拥有相机
     *
     * @return 如果有返回true,没有返回false
     */
    public static boolean checkCameraHardware(Context context) {
        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
            // 有相机
            Log.i(TAG, "有相机");
            return true;
        } else {
            // 没有相机
            Log.i(TAG, "没有相机");
            return false;
        }
    }

代码比较简单,比较坑爹的是什么呢?有一些嵌入式设备的Android系统通过这个方法是无法获取到到底特么的是有没有相机的。获取的可能是错误的信息,我在我们的设备上用这个代码检测,明明没有相机也判断成有相机了。如果一定要判断……还是有办法的,直接Camera.open试一试,成功就说明有,失败就……就失败了呗。

访问相机

访问相机的代码比较简单,就是通过Camera.open方法拿到一个Camera的实例,需要注意的是这个open方法可能会引发异常,最好还是要try catch一下的。

    /**
     * 获取前置相机实例,注意6.0以上的系统需要动态申请权限(如果
     * target >= 23)则必须动态申请,否则无法打开相机
     *
     * @return 打开成功则返回相机实例,失败则返回null
     */
    public static Camera getCameraInstance() {
        Camera c;
        try {
            c = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
        } catch (Exception e) {
            e.printStackTrace();
            // 相机正在使用或者不存在
            Log.e(TAG, "相机打开失败,正在使用或者不存在,或者,没有权限?");
            return null;
        }
        return c;
    }

Android 2.3版本以后可以通过Camera.open(int)来打开指定的相机,我这里打开了后置摄像头。

创建预览类

预览类就是用来播放相机画面的类,预览类是继承自SurfaceView的。普通的View以及其子类都是共享同一个surface的,所有的绘制都必须在UI线程进行。而SurfaceView是一种比较特殊的view,他并不与其他view共享surface,而是在内部持有了一个独立的surface,SurfaceView负责管理这个surface的格式、尺寸以及显示位置。由于UI线程还要同事处理其他交互逻辑,因此对View的更新速度和帧率无法保证,而surfaceview由于持有一个独立的surface,因而可以在独立的线程中进行绘制,因此可以提供更高的帧率。自定义相机的预览图像由于对更新速度和帧率要求比较高,所以比较适合用surfaceview来显示。(关于surfaceview的介绍摘自Android自定义相机详细讲解)

介绍了这么多,对SurfaceView大概有个了解就可以了,这个类和相机的声明周期息息相关,需要实现SurfaceHolder.Callback接口来接收View创建和销毁的事件。代码如下:

package com.xiasuhuei321.cameradieorme.camera;

/**
 * Created by xiasuhuei321 on 2017/8/22.
 * author:luo
 * e-mail:[email protected]
 */

import android.content.Context;
import android.hardware.Camera;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;


public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback, Camera.AutoFocusCallback, Camera.PictureCallback {
    public static final String TAG = "CameraPreview";
    public static final String DIRNAME = "MyCamera";

    private SurfaceHolder mHolder;
    private Camera mCamera;
    private boolean canTake = false;
    private Context context;

    public CameraPreview(Context context) {
        super(context);
        this.context = context;
        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = getHolder();
        // deprecated setting, but required on Android versions prior to 3.0
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        Log.i(TAG, "CameraPreview被创建 " + this.hashCode());
    }

    /**
     * surface在很多情况下都会被销毁,这个时候相机也会被释放。
     * 而这个类的camera就无法再使用了,所以需要外部再传入一个
     * 正确的Camera实例
     *
     * @param mCamera Camera实例
     */
    public void setCamera(Camera mCamera) {
        this.mCamera = mCamera;
        mHolder.addCallback(this);
        surfaceCreated(getHolder());
        Log.i(TAG, "serCamera" + " release = " + CameraUtil.getInstance().isRelease());
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // surface创建完毕,camera设置预览
        Log.i(TAG, "surface view被创建");
        if (CameraUtil.getInstance().isRelease()) return;
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // 在这里可以释放相机资源
        // 也可以在Activity中释放
        Log.i(TAG, "surface 被销毁 ");
        holder.removeCallback(this);
        // 停止回调,以防释放的相机再被使用导致异常
        mCamera.setPreviewCallback(null);
        // 停止预览
        mCamera.stopPreview();
        mCamera.lock();
        // 释放相机资源
        CameraUtil.getInstance().releaseCamera();
        mCamera = null;

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // If your preview can change or rotate, take care of those events here.
        // Make sure to stop the preview before resizing or reformatting it.
        if (mHolder.getSurface() == null) {
            // preview surface does not exist
            return;
        }

        // stop preview before making changes
        try {
            mCamera.stopPreview();
        } catch (Exception e) {
            // ignore: tried to stop a non-existent preview
        }

        // set preview size and make any resize, rotate or
        // reformatting changes here

        // start preview with new settings
        try {
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();

        } catch (Exception e) {
            Log.d(TAG, "Error starting camera preview: " + e.getMessage());
        }

    }

    /**
     * 给外部调用,用来拍照的方法
     */
    public void takePhoto() {
        // 因为设置了聚焦,这里又设置了回调对象,所以重新开始预览之后
        // 需要一个标志判断是否是拍照的聚焦回调
        canTake = true;
        // 首先聚焦
        mCamera.autoFocus(this);
//        mCamera.takePicture(null, null, this);
    }

    @Override
    public void onAutoFocus(boolean success, final Camera camera) {
        Log.i(TAG, "聚焦: " + canTake);
        // 不管聚焦成功与否,都开始拍照
        if (canTake) {
            camera.takePicture(null, null, CameraPreview.this);
        }
        canTake = false;
        // 延时一秒,重新开始预览
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                mCamera.startPreview();
            }
        }, 1000);
    }

    @Override
    public void onPictureTaken(final byte[] data, Camera camera) {
        Log.i(TAG, "onPictureTaken");
        // 在子线程中进行io操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                saveToSd(data);
            }
        }).start();
    }


    /**
     * 将照片保存至sd卡
     */
    private void saveToSd(byte[] data) {
        // 创建位图,这一步在图片比较大的时候可能会抛oom异常,所以跳过这一步,直接将byte[]
        // 数据写入文件,而且如果有进行图片处理的需求,尽量不要另外再申请内存,不然很容易
        // oom。所以尽量避免在这里处理图片
//        Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
        // 系统时间
        long dateTaken = System.currentTimeMillis();
        // 图像名称
        String fileName = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken).toString() + ".jpg";

        FileOutputStream fos = null;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String filePath = Environment.getExternalStorageDirectory() + File.separator +
                    DIRNAME + File.separator + fileName;
            Log.i(TAG, "文件路径:" + filePath);
            File imgFile = new File(filePath);
            if (!imgFile.getParentFile().exists()) {
                imgFile.getParentFile().mkdirs();
            }
            try {
                if (!imgFile.exists()) {
                    imgFile.createNewFile();
                }

                fos = new FileOutputStream(imgFile);
//                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
                fos.write(data);
                fos.flush();
                insertIntoMediaPic();
            } catch (Exception e) {

            } finally {
                try {
                    if (fos != null) {
                        fos.close();//关闭
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        } else {
            // sd卡状态异常,直接插入系统相册
            // 暂时是空实现
            insertIntoMediaPic();
        }

    }

    private void insertIntoMediaPic() {

    }
}

这个类可以说是字字血泪,各位看的时候可以结合注释看……每一个我被坑过的地方我都详细的注释了出来。真的是都在代码里了。关于图片处理那一块我是没什么比较好的办法,内存所限,在拿到byte[] data 这个图片数据数组,我直接在转成Bitmap那一步就OOM了,后来看了一下我这里选取的是4160 * 2340的分辨率,直接写入文件一张图也有4~5M,这个时候的问题就是生成一个Bitmap需要申请很大的内存,而原来的data数组因为这个方法还没结束也无法释放(Java参数传递是引用拷贝传递,所以这时候依然有引用指向内存中的data对象,GC无法回收这块内存),所以就算后续你不额外申请内存,有方法在原有的Bitmap对象上进行操作,也是不行的,因为在生成的时候就OOM了。

创建预览布局

这里Android文档中的建议是在布局中放置一个FrameLayout作为相机预览类的父容器,我采用了这种做法,布局如下:




    

    

布局比较简单,就一个FrameLayout和一个ImageView,点击ImageView开始拍照。

接下来看一下Activity的代码:

package com.xiasuhuei321.cameradieorme.camera;

import android.Manifest;
import android.animation.ObjectAnimator;
import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toast;

import com.xiasuhuei321.cameradieorme.R;

/**
 * Created by xiasuhuei321 on 2017/8/22.
 * author:luo
 * e-mail:[email protected]
 */

public class CameraActivity extends AppCompatActivity {

    private Camera camera;
    private FrameLayout preview;
    private CameraPreview mPreview;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_camera);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
        View iv_take = findViewById(R.id.iv_take);

        final ObjectAnimator scaleX = ObjectAnimator.ofFloat(iv_take, "scaleX", 1f, 0.8f);
        final ObjectAnimator scaleY = ObjectAnimator.ofFloat(iv_take, "scaleY", 1f, 0.8f);


        iv_take.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        v.setScaleX(0.9f);
                        v.setScaleY(0.9f);
                        scaleX.start();
                        scaleY.start();
                        break;
                    case MotionEvent.ACTION_UP:
                        v.setScaleX(1f);
                        v.setScaleY(1f);
                        scaleX.reverse();
                        scaleY.reverse();
                        break;
                }
                return false;
            }
        });
        iv_take.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPreview.takePhoto();
            }
        });

        mPreview = new CameraPreview(this);
        CameraUtil.getInstance().init(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 检查权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
                PackageManager.PERMISSION_GRANTED) {
            // 申请权限
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1);
        } else {
            // 已有权限
            startCameraPre();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 如果权限申请成功
            startCameraPre();
        } else {
            Toast.makeText(this, "您已拒绝打开相机,想要使用此功能请手动打开相机权限", Toast.LENGTH_SHORT).show();
        }
    }

    private void startCameraPre() {
        if (CameraUtil.checkCameraHardware(this)) {
            camera = CameraUtil.getInstance().getCameraInstance();
        }
        mPreview.setCamera(camera);
        preview = (FrameLayout) findViewById(R.id.camera_preview);
        if (preview.getChildCount() == 0)
            preview.addView(mPreview);
    }
}

在开始的时候写了两个属性动画,用户在点击的时候有点交互的感觉(貌似并没有什么luan用)。在onResume中检查是否拥有权限打开相机,因为6.0以上需要动态申请啊,蛋疼。拥有权限或者用户给了权限就执行startCameraPre方法,这个方法通过我自己写的CameraUtil获取并初始化了一个Camera实例。并且最后判断FrameLayout中是否有子View,如果没有就将我们自己的相机预览类添加进去。这样打开相机和拍照的整个流程就完成了,当然了,最后还得贴一下CameraUtil代码:

package com.xiasuhuei321.cameradieorme.camera;

import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.hardware.Camera;
import android.util.Log;
import android.view.WindowManager;

import java.util.List;

/**
 * Created by xiasuhuei321 on 2017/8/21.
 * author:luo
 * e-mail:[email protected]
 */

public class CameraUtil {
    public static final String TAG = "CameraUtil";

    private Camera camera;
    private int cameraId;

    private int mScreenWidth;
    private int mScreenHeight;


    //    private Callback callback;
    private boolean release = false;
    private Camera.Parameters params;

    private CameraUtil() {
    }


    private static class CameraUtilHolder {
        private static CameraUtil instance = new CameraUtil();
    }

    public static CameraUtil getInstance() {
        return CameraUtilHolder.instance;
    }

    public void init(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Point p = new Point();
        wm.getDefaultDisplay().getSize(p);
        mScreenWidth = p.x;
        mScreenHeight = p.y;
    }

    /**
     * 检查是否拥有相机
     *
     * @return 如果有返回true,没有返回false
     */
    public static boolean checkCameraHardware(Context context) {
        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
            // 有相机
            return true;
        } else {
            // 没有相机
            return false;
        }
    }

    /**
     * 获取前置相机实例,注意6.0以上的系统需要动态申请权限(如果
     * target >= 23)则必须动态申请,否则无法打开相机
     *
     * @return 打开成功则返回相机实例,失败则返回null
     */
    public Camera getCameraInstance() {
        if (camera != null) {
            Log.i(TAG, "camera已经打开过,返回前一个值");
            return camera;
        }
        try {
            camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
            cameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
        } catch (Exception e) {
            e.printStackTrace();
            // 相机正在使用或者不存在
            Log.i(TAG, "相机打开失败,正在使用或者不存在,或者,没有权限?");
            return null;
        }
        initParam();
        release = false;
        return camera;
    }

    public void initParam() {
        if (camera == null) {
            return;
        }
        if (params != null) {
            camera.setParameters(params);
        } else {
            camera.setParameters(generateDefaultParams(camera));
        }
    }

    /**
     * 允许从外部设置相机参数
     *
     * @param params 相机参数
     */
    public void setParams(Camera.Parameters params) {
        this.params = params;
    }

    /**
     * 生成默认的相机参数
     *
     * @param camera 使用该参数的相机
     * @return 生成的参数
     */
    public Camera.Parameters generateDefaultParams(Camera camera) {
        Camera.Parameters parameters = camera.getParameters();
        // 设置聚焦
        if (parameters.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);// 连续对焦模式
        }
        camera.cancelAutoFocus();//自动对焦。
        // 设置图片格式
        parameters.setPictureFormat(PixelFormat.JPEG);
        // 设置照片质量
        parameters.setJpegQuality(100);
        if (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
            // 默认打开前置摄像头,旋转90度即可
            camera.setDisplayOrientation(90);
            parameters.setRotation(90);
        } else if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            // 打开后置摄像头,旋转270,这个待验证
            camera.setDisplayOrientation(270);
            parameters.setRotation(180);
        }

        // 获取摄像头支持的PictureSize列表
        List picSizeList = parameters.getSupportedPictureSizes();
        for (Camera.Size size : picSizeList) {
            Log.i(TAG, "pictureSizeList size.width=" + size.width + "  size.height=" + size.height);
        }
        Camera.Size picSize = getProperSize(picSizeList, ((float) mScreenHeight / mScreenWidth));
        parameters.setPictureSize(picSize.width, picSize.height);

        // 获取摄像头支持的PreviewSize列表
        List previewSizeList = parameters.getSupportedPreviewSizes();
        for (Camera.Size size : previewSizeList) {
            Log.i(TAG, "previewSizeList size.width=" + size.width + "  size.height=" + size.height);
        }
        Camera.Size preSize = getProperSize(previewSizeList, ((float) mScreenHeight) / mScreenWidth);
        Log.i(TAG, "final size is: " + picSize.width + " " + picSize.height);
        if (null != preSize) {
            Log.i(TAG, "preSize.width=" + preSize.width + "  preSize.height=" + preSize.height);
            parameters.setPreviewSize(preSize.width, preSize.height);
        }

        return parameters;
    }

    private Camera.Size getProperSize(List pictureSizeList, float screenRatio) {
        Log.i(TAG, "screenRatio=" + screenRatio);
        Camera.Size result = null;
        for (Camera.Size size : pictureSizeList) {
            float currentRatio = ((float) size.width) / size.height;
            if (currentRatio - screenRatio == 0) {
                result = size;
                break;
            }
        }

        if (null == result) {
            for (Camera.Size size : pictureSizeList) {
                float curRatio = ((float) size.width) / size.height;
                if (curRatio == 4f / 3) {// 默认w:h = 4:3
                    result = size;
                    break;
                }
            }
        }

        return result;
    }

    /**
     * 释放相机资源
     */
    public void releaseCamera() {
        if (camera != null) {
            camera.release();
        }
        camera = null;
        release = true;
    }

    /**
     * 现在是否处于释放状态
     *
     * @return true释放,false没释放
     */
    public boolean isRelease() {
        return release;
    }

}

这里需要注意的就是生成相机参数那一块了,Android中的相机默认是横向的,我们平时用的时候肯定不是那么用的,所以通过 camera.setDisplayOrientation(90)旋转90度调整一下。不过设置了这个之后,如果不设置parameters.setRotation(90)那么保存的图片方向也不对,设置了这个之后就可以了。不过我看网上很多都是采用自己生成Bitmap然后自己旋转……如果parameters.setRotation(90)这种方式可以完成的话,最好不要采用自己处理的方式了,内存开销太大了。关于资源的释放啊什么的,都在预览类里面注释写好了= = ,这里就不再赘述了。

小结

最开始研究相机是因为项目里一个用到相机三方总是报错,在有空研究了一下相机之后,添了一行代码,测试到现在还算比较稳定,没有出现崩溃了,有的时候真的是,一行代码能改变的东西却是非常多的。跑题了跑题了,现在突然感觉相机可以玩的东西很多……以后这个demo可能会继续完善

代码地址:https://github.com/ForgetAll/CameraDieOrMe

参考资料

  • 官方文档
  • Android自定义相机详解
  • Android手把手带你玩转自定义相机

你可能感兴趣的:(详记Android打开相机拍照流程)