简单谈谈android自定义相机的实现(上 android.hardware.Camera)

通常情况下,调用android系统相机基本上可以满足拍照的需求,而自定义相机的需求一般来自于开发自己的相机应用,今天我们来简单聊聊android自定义相机的实现,限于篇幅,我们上篇只讨论android.hardware.Camera,下篇我会和大家一起讨论一下android.hardware.Camera2

demo代码

  1. 基础介绍
  2. 实现一个简单的拍照demo
  3. 讨论

  1. 基础介绍


首先简单看一下用到的类有哪些:

类名 介绍 注释
Camera android 5.0 之前控制相机硬件是通过这个Camera类实现的 deprecated in API level 21.
Camera.Parameters 控制相机的参数,如对焦,闪光灯等 deprecated in API level 21.
SurfaceView 专注于绘制应用层内嵌浮层的类,这里我们用它来预览镜头的取景 \
SurfaceHolder 持有一个展示的surface的抽象接口。允许你控制surface的尺寸和格式,编辑surface中的像素和监听surface的改变。 \
SurfaceHolder.Callback 控制该接口的客户端能够接收surface改变的信息。 \

2. demo实现

  1. 添加权限
  2. 简单布局
  3. 打开相机
  4. 设置预览
  5. 拍照及保存
  6. 释放资源

添加权限

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

在camera的官方接口文档中,给出了控制camera的10个步骤,很详细,我们这边侧重于实现。


简单布局


一个简单的界面,一个拍照键Button,一个FrameLayout用于填充Surface

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/camera_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Camera.CameraActivity">

    <FrameLayout
        android:id="@+id/camera_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"/>

    <LinearLayout
        android:id="@+id/control"
        android:layout_width="match_parent"
        android:layout_height="112dp"
        android:layout_alignParentBottom="true"
        android:background="@color/control_background"
        android:gravity="center"
        android:orientation="horizontal">

            <Button
                android:id="@+id/camera_take_photo"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="@string/picture" />
    LinearLayout>
RelativeLayout>

简单谈谈android自定义相机的实现(上 android.hardware.Camera)_第1张图片


**打开相机**

这里包括了检测系统相机状态,指定打开具体的相机设备,处理异常等

/**
* 初始化相机
*/
private void initCamera() {
   if(!getPackageManager().hasSystemFeature(
       PackageManager.FEATURE_CAMERA)) {
     //TODO 未检测到系统的相机
} else {
     //获取cameraId ,在api8及以前,直接调用Camera.open()方法会打开后置摄像头
     //获取系统后置摄像头的id(工具方法,后面附代码)
     int cameraId = CameraUtil.findBackFacingCamera();
     if (!CameraUtil.isCameraIdValid(cameraId)) {
          //TODO 检测camera id无效
     } else {
         //是否可以安全打开相机
         if (safeCameraOpen(cameraId)) {
           mCamera.startPreview();  //开启相机预览
           mPreview.setCamera(mCamera);
         } else {             
             //TODO 无法安全打开相机    
         }
     }
 }
//打开相机的操作延迟到onResume()方法里面去执行,这样可以使得代码更容易重用,还能保持控制流程更为简单。当然也可以另起线程处理
@Override
public void onResume() {
    super.onResume();
    initCamera();
}
附上获取相机id的工具方法:
public final class CameraUtil {

    public static final int INVALID_CAMERA_ID = -1;

    /**
     * 获取前置摄像头id
     * @return
     */
    public static int findFrontFacingCamera() {
        int cameraId = INVALID_CAMERA_ID;
        // Search for the front facing camera
        int numberOfCameras = Camera.getNumberOfCameras();
        for (int i = 0; i < numberOfCameras; i++) {
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(i, info);
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                Log.d("CameraUtil", "Camera found");
                cameraId = i;
                break;
            }
        }
        return cameraId;
    }

    /**
     * 获取后置摄像头id
     * @return
     */
    public static int findBackFacingCamera() {
        int cameraId = INVALID_CAMERA_ID;
        // Search for the front facing camera
        int numberOfCameras = Camera.getNumberOfCameras();
        for (int i = 0; i < numberOfCameras; i++) {
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(i, info);
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                Log.d("CameraUtil", "Camera found");
                cameraId = i;
                break;
            }
        }
        return cameraId;
    }

    public static boolean isCameraIdValid(int cameraId) {
        return cameraId != INVALID_CAMERA_ID;
    }

}
处理打开相机时可能出现的异常:
    /**
     * 获取Camera,并加入开启检测
     *
     * @param id 相机id
     * @return
     */
    private boolean safeCameraOpen(int id) {
        boolean qOpened = false;
        try {
            releaseCameraAndPreview();
            mCamera = Camera.open(id);
            qOpened = (mCamera != null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return qOpened;
    }
这样不出意外,我们成功的打开了相机 —

**设置取景的预览**

这里包括了设置相机参数,设置相机方向,初始化SurfaceHolder从而来setPreviewDisplay(SurfaceHolder) ,完成这一步,即可实现取景的预览。

一般的,我们可以在布局中放置一个默认的SurfaceView,也可以通过addView()方法将SurfaceView实例添加到指定的容器中。
这里我们通过继承SurfaceView,派生一个自定义的CameraPreview,同时实现SurfaceHolder.Callback接口便于我们对surface进行控制。

public class CameraPreview extends SurfaceView implements           SurfaceHolder.Callback {

    private static final String TAG = "TAG";

    /**
     * 控制相机方向
     */
    private static final SparseIntArray ORIENTATIONS = new SparseIntArray();

    static {
        ORIENTATIONS.append(Surface.ROTATION_0, 90);
        ORIENTATIONS.append(Surface.ROTATION_90, 0);
        ORIENTATIONS.append(Surface.ROTATION_180, 270);
        ORIENTATIONS.append(Surface.ROTATION_270, 180);
    }

    private SurfaceHolder mHolder;
    private Camera mCamera;

    //持有Activity引用,为了获取屏幕方向,改成内部类会比较好
    private Activity mActivity;

    public CameraPreview(Activity activity) {
        super(activity);
        mActivity = activity;
        mHolder = getHolder();
        mHolder.addCallback(this);
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            //API 11及以后废弃,需要时自动配置
           mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        }
    }

    public void setCamera(Camera camera) {
        mCamera = camera;
    }

    /**
     * 刷新相机
     */
    private void refreshCamera() {
        if (mCamera != null) {
            requestLayout();
            //获取当前手机屏幕方向
            int rotation = mActivity.getWindowManager()
                .getDefaultDisplay().getRotation();
            //调整相机方向            
            mCamera.setDisplayOrientation(
            ORIENTATIONS.get(rotation));
            // 设置相机参数
            mCamera.setParameters(settingParameters());
        }
    }

    /**
     * 配置相机参数
     * @return
     */
    private Camera.Parameters settingParameters() {
        // 获取相机参数
        Camera.Parameters params = mCamera.getParameters();
        List focusModes = params.getSupportedFocusModes();
        //设置持续的对焦模式
        if (focusModes.contains(
        Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
        params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        }

        //设置闪光灯自动开启
        if (focusModes.contains(Camera.Parameters.FLASH_MODE_AUTO)) {
            params.setFocusMode(Camera.Parameters.FLASH_MODE_AUTO);
        }

        return params;
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, now tell the camera where to draw the preview.
        try {
            if(mCamera != null) {
                //surface创建,设置预览SurfaceHolder
                mCamera.setPreviewDisplay(holder);
                //开启预览
                mCamera.startPreview();
            }
        } catch (IOException e) {
            Log.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // empty. Take care of releasing the Camera preview in your activity.
    }

    @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
        refreshCamera();
        // start preview with new settings
        try {
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();

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

CameraPreview里面涉及到预览主要的逻辑,一些如对焦模式或闪光灯模式等相机参数,大家可以自己参照API文档。接下来只要在onCreate()方法里,将我们的SurfaceView加入我们设置好的布局容器中即可。

mPreview = new CameraPreview(this);
FrameLayout vPreview = (FrameLayout) findViewById(R.id.camera_preview);
vPreview.addView(mPreview);

实现拍照


设置按钮的点击事件的代码就不贴出来了,主要说说如何拍照,以及将照片储存在文件中。

拍照很简单,只需要调用takePicture (Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg)方法

Camera.ShutterCallback 发生在照片刚被捕获的一瞬间

后面两个回调分别发生在原始的照片数据/压缩的jpeg照片数据可用时,api5之后,额外提供了一个4个回调参数的方法,大家自己看文档就好。这里我们简化处理,只提供最后一个回调:

//触发一个异步的图片捕获回调
mCamera.takePicture(null, null, this);
...
@Override
public void onPictureTaken(final byte[] data, Camera camera) {
    File pictureFile = ...; //自行创建一个file文件用于保存照片
    OutputStream output = null;
    try {
        output = new FileOutputStream(pictureFile);
        output.write(data);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (output != null) {
            try {
                output.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //保存成功后的处理
    resetCamera();
}

其实很简单,只需要将字节数据写进文件里即可,如果保存完图片后想继续拍照,这时候就需要重置一下相机,因为拍照完,取景预览默认定格在取景页,提供一下resetCamera()方法,供大家参考

/**
* 重置相机
*/
private void resetCamera() {
   mCamera.startPreview();
   mPreview.setCamera(mCamera);
}

相机资源的释放

/**
* 释放相机和预览
*/
private void releaseCameraAndPreview() {
   mPreview.setCamera(null);
   if (mCamera != null) {
       mCamera.release();
       mCamera = null;
   }
}

如果我们开启相机是activity的onResume()方法中,那么我们可以在onPause()方法中释放相机和预览资源

@Override
protected void onPause() {
   super.onPause();
   releaseCameraAndPreview();
}

补充


(1). 一般情况下,我们无需自己实现拍照。不过有些特殊的需求还必须自己控制拍照过程。
例如,我们想获取照片拍摄的 精准时间,一般我们只能拿到照片的updateTime。即使通过解析照片文件的方式获取到createTime,也和照片实际的拍摄时间有误差。原因很简单,大家看第三步
takePicture (Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg)方法的回调,createTime实际上是字节数据写入file中的时刻。举个例子,我们设置微信头像,选择拍照,微信会调取系统相机,这时,我们点击拍照,成功,进入系统提示是否保存的页面,如图:
简单谈谈android自定义相机的实现(上 android.hardware.Camera)_第2张图片

大家看,这时图片还是以字节的形式存在,只有当用户点击保存,系统才会保存图片。所以文件的创建时间其实并不是拍照的那一刻(虽然也差不多)。所以自己控制camera拍照的话就可以拿到相对精准的照片拍摄时间。

留个坑:android中如何获取照片(文件)的创建时间?

目测:基于linux的android也只能拿到lastModified time

(2). SurfaceView 生命周期的控制
还是上一个图,例如我们拍照后进入图中状态,这时锁屏或者进入后台(触发Activity的onPause()方法),然后我们再返回应用,此时我们会发现,SurfaceView原本固定的取景完全恢复了。首先我们想到的处理方法是设置状态机,
定义整个拍照过程的不同状态,如:

    /**
     * 预览中
     */
    private static final int STATE_PREVIEW = 0;
    /**
     * 等待保存
     */
    private static final int STATE_WAITING_SAVE = 1;
    /**
     * 保存中
     */
    private static final int STATE_SAVING = 2;

在onResume()和onPause()方法中,针对不同的状态,控制初始化/释放的过程。
但是,SurfaceView默认的生命周期和activity也是相关的,所以如果希望在上述操作中保持SurfaceView的状态,需要额外一些操作。
再留一个坑:SurfaceView生命周期与Activity的生命周期

(3). 拍照方向
android相机传感器默认是横向的,当竖屏拍照时,照片默认是旋转90度的
第三个坑:照片旋转了如何处理?

你可能感兴趣的:(安卓,移动开发)