写在前面
本文并不是基于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手把手带你玩转自定义相机