前言
在开发Android应用的时候,如果需要调用摄像头拍照或者录像,除了通过Intent调用系统现有相机应用进行拍照录像之外,还可以通过直接调用Camera硬件去去获取摄像头进行拍照录像的操作。本篇博客将讲解如何在Android应用中通过Camera拍照功能.
录像功能因为需要与MediaRecorder配使用,反而是更偏向操作MediaRecorder,所以我把录像功能放到了音视频开发篇幅里,请参考以下博客了解录像功能开发.
MediaRecorder视频录制入门:https://www.cnblogs.com/guanxinjing/p/10980906.html
MediaRecorder与Camera1配合使用:https://www.cnblogs.com/guanxinjing/p/10986766.html
拍照开发
流程
- 获取权限
- 初始化曲面视图View(SurfaceView或者TextureView)(用于显示相机预览图像)
- 初始化打开相机,选择前后摄像头
- 配置相机参数
- 拍照
- 处理照片返回数据,旋转照片与压缩照片
获取权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
拍照TextureView例子
其实目前最适合相机预览图像显示使用的是TextureView,因为它是真正独立刷新帧数的View,可以让相机预览减少卡顿问题
而使用TextureView需要开启硬件加速功能.开启硬件加速方法如下:
在AndroidManifest.xml 清单文件里,你需要实现相机功能的activity添加 android:hardwareAccelerated="true"
<activity android:name=".work.share.FaceCameraActivity" android:hardwareAccelerated="true">activity>
以下是代码部分:
public class FaceCameraActivity extends BaseActivity implements TextureView.SurfaceTextureListener,Camera.PictureCallback , View.OnClickListener{ private static final float PICTURE_SIZE_PROPORTION = 1.1f;//目标分辨率尺寸 private TextureView mTextureView; private Camera mCamera; private Button mBtnCamera; private boolean mMoveTakingPhotos = false; //用于防止连续点击拍照多次引起报错的问题 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initCamera(); initCameraParameters(); } @Override public int getLayout() { return R.layout.activity_face_camera; } @Override public void initView() { mTextureView = findViewById(R.id.texture_view); mBtnCamera = findViewById(R.id.btn_camera); mBtnCamera.setOnClickListener(this); mTextureView.setSurfaceTextureListener(this);//添加监听,用于监听TextureView的创建/变化/销毁 } @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_camera: if(!mMoveTakingPhotos){ mMoveTakingPhotos = true; mCamera.takePicture(null,null,this);//拍摄拍照 参数为快门图片回调/原始图片回调(未压缩)/jpeg图片回调 } break; default: break; } } /** * 初始化打开相机 */ private void initCamera(){ if (mCamera == null){ //大多数情况下:0是后置摄像头 1是前置摄像头 ,这里demo就不弄这么复杂,在下面的会提供选择前后摄像头的方法 mCamera = Camera.open(1); } } /** * 初始化相机参数 */ private void initCameraParameters(){ Camera.Parameters parameters = mCamera.getParameters(); // parameters.getPreviewSize();//当前预览尺寸 // parameters.getPictureSize();//当前分辨率尺寸 // parameters.getJpegThumbnailSize(); //返回当前jpeg图片中exif缩略图的尺寸 // parameters.getSupportedPreviewSizes();//预览尺寸List // parameters.getSupportedPictureSizes();//分辨率尺寸List // parameters.getSupportedJpegThumbnailSizes();//返回当前jpeg图片中exif缩略图的尺寸List DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); Camera.Size previewSize = getpreviewSize(parameters, displayMetrics.heightPixels, displayMetrics.widthPixels);//获取最接近屏幕分辨率的的预览尺寸 Camera.Size pictureSize = getPictureSize(parameters, PICTURE_SIZE_PROPORTION);//获取对应比例的最大分辨率 parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);//设置关闭闪光灯 parameters.setFocusMode(Camera.Parameters.FLASH_MODE_AUTO); //对焦设置为自动 parameters.setPictureFormat(PixelFormat.JPEG);//拍照格式 parameters.setPreviewSize(previewSize.width, previewSize.height);//设置预览尺寸 parameters.setPictureSize(pictureSize.width, pictureSize.height);//分辨率尺寸 parameters.set("orientation", "portrait");//相片方向 parameters.set("rotation", 90); //相片镜头角度转90度(默认摄像头是横拍) mCamera.setParameters(parameters);//添加参数 mCamera.setDisplayOrientation(90);//设置显示方向 } /** * 计算获得最接近比例的预览尺寸 * @param parameters * @param height * @param width * @return */ private Camera.Size getpreviewSize(Camera.Parameters parameters, int height, int width){ ListpreviewSizeList = parameters.getSupportedPreviewSizes(); Camera.Size selectPreviewSize = null; //缓存当前最准确比例的Size float currentDifference = 0; //缓存当前最小的差值 /** * 下面这行代码是求传入高度和宽度的高宽比例,这里可以发现一个细节我下面的预览尺寸求的的宽高比例. * 是的他们一个是高宽比一个是宽高比,说明为什么这样,因为如果按照2个都是高宽比来获得预览尺寸你会发现,获得的尺寸怎么都有可能会拉伸变形(除非狗屎运尺寸完美刚好) * 最好的办法就是,不求最合适目标尺寸的长方形比例,而求一个最适合目标尺寸的正方形比例,这样拉伸变形就不会出现了 */ float proportion = (float)height/(float)width; for (int i = 0; i < previewSizeList.size(); i++){ Camera.Size size = previewSizeList.get(i); float previewSizeProportion = ((float)size.width)/((float)size.height); //计算当前预览尺寸的宽高比例 float tempDifference = Math.abs(previewSizeProportion - proportion); //相减求绝对值的差值 if(i == 0){ selectPreviewSize = size; currentDifference = tempDifference; continue; } if (tempDifference <= currentDifference){ //获得最小差值 if (tempDifference == currentDifference){ //如果差值一样 if ((selectPreviewSize.width + selectPreviewSize.height) < (size.width+size.height)){ //判断那个尺寸大保留那个 selectPreviewSize = size; currentDifference = tempDifference; } }else { //如果差值更小更准确 selectPreviewSize = size; currentDifference = tempDifference; } } L.e("currentDifference="+currentDifference +"width="+selectPreviewSize.width+"height="+selectPreviewSize.height); } return selectPreviewSize; } /** * 计算获得最接近比例的分辨率 * @param parameters * @param targetProportion * @return */ private Camera.Size getPictureSize(Camera.Parameters parameters, float targetProportion) { List pictureSizeList = parameters.getSupportedPictureSizes(); Camera.Size selectPreviewSize = null; float currentDifference = 0; for (int i = 0; i < pictureSizeList.size(); i++) { Camera.Size size = pictureSizeList.get(i); L.e("分辨率列表_" + i + "_width=" + size.width + "height=" + size.height); float pictureSizeProportion = ((float) size.width) / ((float) size.height); L.e("分辨率列表_" + i + "_比例=" + pictureSizeProportion); float tempDifference = Math.abs(pictureSizeProportion - targetProportion); if (i == 0) { selectPreviewSize = size; currentDifference = tempDifference; continue; } if (tempDifference <= currentDifference) { if (tempDifference == currentDifference) { if ((selectPreviewSize.width + selectPreviewSize.height) < (size.width + size.height)) { //判断那个尺寸大保留那个 selectPreviewSize = size; currentDifference = tempDifference; } } else { //如果差值更小更准确 selectPreviewSize = size; currentDifference = tempDifference; } } } L.e("当前选择分辨率width=" + selectPreviewSize.width + "height=" + selectPreviewSize.height); return selectPreviewSize; } /** * 照片拍照完成后的回调方法 * @param data * @param camera */ @Override public void onPictureTaken(final byte[] data, Camera camera) { //注意!此处返回是主线程,而处理图片是耗时操作需要放到子线程里处理 handlerImageWaitDialog().show(); new Thread(new Runnable() { @Override public void run() { try { //这里有一个坑,如果你想要读取照片的角度信息,那么就需要直接吧byte[] data的照片数据先保存成图片文件在从文件读成Bitmap //因为如果你先处理压缩图片或者裁剪图片,只要是Bitmap.createBitmap处理过就都有可能丢失这些照片信息到时候你怎么获取角度都是0 FilePathSession.deleteFaceImageFile(); FileOutputStream fileOutputStream = new FileOutputStream(FilePathSession.getFaceImagePath()); fileOutputStream.write(data,0,data.length); fileOutputStream.flush(); fileOutputStream.close(); int angle = readPictureDegree(FilePathSession.getFaceImagePath().toString()); //获取角度 Bitmap bitmap = BitmapFactory.decodeFile(FilePathSession.getFaceImagePath().toString());//重新在文件里获取图片 Matrix matrix = new Matrix();//创建矩阵配置类,用与设置旋转角度和旋转位置 matrix.setRotate(angle, bitmap.getWidth(), bitmap.getHeight());//设置旋转角度和旋转位置 Bitmap handlerAngleBitmap = Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true); ImageHandle.bitmapImageConfig(handlerAngleBitmap)//这个是个人写的压缩图片工具类 .setTargetKB(200) .setSize(1080f,1920f) .setHandleListener(new BitmapImageHandleListener() { @Override public boolean onReady(Bitmap inpBitmap) { return true; } @Override public void onSuccess(Bitmap outBitmap) { try { FileOutputStream fileOutputStream = new FileOutputStream(FilePathSession.getFaceImagePath()); outBitmap.compress(Bitmap.CompressFormat.JPEG,90,fileOutputStream); fileOutputStream.flush(); fileOutputStream.close(); outBitmap.recycle(); runOnUiThread(new Runnable() { @Override public void run() { handlerImageWaitDialog().dismiss(); Intent startFaceConfirm = new Intent(FaceCameraActivity.this, FaceConfirmActivity.class); startActivity(startFaceConfirm); FaceCameraActivity.this.finish(); } }); } catch (IOException e) { e.printStackTrace(); runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "图像压缩处理失败", Toast.LENGTH_SHORT).show(); } }); } } @Override public void onFailure(String text) { runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "图像压缩处理失败", Toast.LENGTH_SHORT).show(); } }); } @Override public void onError(Exception e) { runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "图像压缩处理失败", Toast.LENGTH_SHORT).show(); } }); } }).build(); } catch (FileNotFoundException e) { e.printStackTrace(); runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "图像压缩处理失败", Toast.LENGTH_SHORT).show(); } }); } catch (IOException e) { e.printStackTrace(); runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "图像压缩处理失败", Toast.LENGTH_SHORT).show(); } }); } } }).start(); } /** * TextureView的创建完成后的可用状态回调 */ @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { //可用 try { mCamera.setPreviewTexture(surface);//给相机添加预览图像的曲面View mCamera.startPreview();//启动图像预览 } catch (IOException e) { e.printStackTrace(); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { //尺寸变化 } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { //销毁 return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { //更新 } /** * 读取照片旋转角度 * * @param path 照片路径 * @return 角度 */ public int readPictureDegree(String path) { int degree = 0; try { ExifInterface exifInterface = new ExifInterface(path); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: L.e("触发 90度"); degree = 90; degree = degree + 90;//这里我是直接处理要增加或者减少的角度,让图片竖起来 break; case ExifInterface.ORIENTATION_ROTATE_180: L.e("触发 180度"); degree = 180; degree = degree + 0; break; case ExifInterface.ORIENTATION_ROTATE_270: L.e("触发 270度"); degree = 270; degree = degree - 90; break; default: L.e("触发 0度"); degree = 0; degree = degree + 180; break; } } catch (IOException e) { e.printStackTrace(); } return degree; } @Override protected void onResume() { super.onResume(); // try { // mCamera.reconnect(); //相机重连接 // mCamera.startPreview(); // } catch (IOException e) { // e.printStackTrace(); // } } @Override protected void onStop() { super.onStop(); // mCamera.stopPreview();暂停预览 } @Override protected void onDestroy() { super.onDestroy(); if (mCamera != null){ mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } }
选择前后摄像头的代码
/** * 选择摄像头 * @param isFacing true=前摄像头 false=后摄像头 * @return 摄像id */ private Integer selectCamera(boolean isFacing){ int cameraCount = Camera.getNumberOfCameras(); // CameraInfo.CAMERA_FACING_BACK 后摄像头 // CameraInfo.CAMERA_FACING_FRONT 前摄像头 int facing = isFacing ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK; Log.e(TAG, "selectCamera: cameraCount="+cameraCount); if (cameraCount == 0){ Log.e(TAG, "selectCamera: The device does not have a camera "); return null; } Camera.CameraInfo info = new Camera.CameraInfo(); for (int i=0; i < cameraCount; i++){ Camera.getCameraInfo(i,info); if (info.facing == facing){ return i; } } return null; }
如果你需要切换摄像头
mCamera.stopPreview();//暂停预览 mCamera.release();//释放摄像头 这个是关键 openCamera(selectCamera(mCurrentCameraFacing));//重新选择摄像头并且打开 configCameraParameters();//重新配置摄像头参数 startPreview(mSurfaceTexture);//开启预览
拍照SurfaceView例子
package com.demo; import androidx.appcompat.app.AppCompatActivity; import android.graphics.PixelFormat; import android.hardware.Camera; import android.os.Bundle; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.widget.Button; import com.example.user.demo.R; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class TakePictureActivity extends AppCompatActivity implements SurfaceHolder.Callback,Camera.PictureCallback { private static final String TAG = "TakePictureActivity"; private Button mBtnTake; private SurfaceView mSurfaceView; private SurfaceHolder mSurfaceHolder; private Camera mCamera; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_take_picture); mBtnTake = (Button)findViewById(R.id.take); mSurfaceView = (SurfaceView)findViewById(R.id.surfaceView); init(); mBtnTake.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mCamera != null){ mCamera.takePicture(null,null,TakePictureActivity.this); } } }); } @Override protected void onDestroy() { super.onDestroy(); mSurfaceHolder.removeCallback(this); mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); } private void init(){ mSurfaceHolder = mSurfaceView.getHolder(); mCamera = Camera.open(); mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mSurfaceHolder.addCallback(this); Camera.Parameters parameters = mCamera.getParameters(); parameters.setFlashMode("off"); parameters.setPictureFormat(PixelFormat.JPEG); //设定相片格式为JPEG,默认为NV21 parameters.setPreviewSize(640, 480); parameters.set("orientation", "portrait");//相片方向 parameters.set("rotation", 90); //相片镜头角度转90度(默认摄像头是横拍) mCamera.setParameters(parameters); mCamera.setDisplayOrientation(90); } @Override public void surfaceCreated(SurfaceHolder holder) { Log.e(TAG,"悬浮窗口生成"); try { mCamera.setPreviewDisplay(mSurfaceHolder); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.e(TAG,"悬浮窗口变化"); } @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.e(TAG,"悬浮窗口销毁"); } @Override public void onPictureTaken(final byte[] data, Camera camera) { Log.e(TAG,"拍照结果处理"); File file = getExternalFilesDir("takePiceture"); if (!file.exists()){ file.mkdirs(); } final File fileName = new File(file,System.currentTimeMillis()+".jpg"); new Thread(new Runnable() { @Override public void run() { try { FileOutputStream fileOutputStream = new FileOutputStream(fileName); fileOutputStream.write(data); fileOutputStream.close(); mCamera.startPreview(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } }
Camera api 说明
Camera是Android摄像头硬件的相机类,位于硬件包"android.hardware.Camera"下。它主要用于摄像头捕获图片、启动/停止预览图片、拍照、获取视频帧等,它是设备本地的服务,负责管理设备上的摄像头硬件。
Camera既然用于管理设备上的摄像头硬件,那么它也为开发人员提供了相应的方法,并且这些方法大部分都是native的,用C++在底层实现,下面简单介绍一下Camera的一些方法:
api | 说明 |
open() | 打开Camera,返回一个Camera实例。 |
open(int cameraId) | 根据cameraId打开一个Camera,返回一个Camera实例。 |
release() | 释放掉Camera的资源。 |
getNumberOfCameras() | 获取当前设备支持的Camera硬件个数。 |
getParameters() | 获取Camera的各项参数设置类。 |
setParameters(Camera.Parameters params) | 通过params把Camera的各项参数写入到Camera中。 |
setDisplayOrientation(int degrees) | 摄像预览的旋转度。 |
setPreviewDisplay(SurfaceHolder holder) | 设置Camera预览的SurfaceHolder。 |
starPreview() | 开始Camera的预览。 |
stopPreview() | 停止Camera的预览 |
setPreviewCallback() | 设置预览回调 |
reconnect() | 重新连接 |
autoFocus(Camera.AutoFocusCallback cb) | 自动对焦 |
cancelAutoFocus() | 取消启动对焦 |
setAutoFocusMoveCallback() | 自动对焦移动回调 |
takePicture(Camera.ShutterCallback shutter,Camera.PictureCallback raw,Camera.PictureCallback jpeg) | 拍照。 |
enableShutterSound() | 启用快门声音 |
lock() | 锁定Camera硬件,使其他应用无法访问。 |
unlock() | 解锁Camera硬件,使其他应用可以访问。 |
startFaceDetection() | 启动人脸识别 |
stopFaceDetection() | 停止人脸识别 |
setFaceDetectionListener() | 人脸识别监听回调 |
setPreviewCallback() | 设置预览回调 |
setPreviewCallbackWithBuffer() | 设置预览缓冲回调 |
setOneShotPreviewCallback() | 设置一个镜头预览回调 |
setErrorCallback() | 设置异常回调 |
startSmoothZoom() | 启动平滑缩放 |
stopSmoothZoom() | 停止平滑缩放 |
setZoomChangeListener() | 缩放监听 |
setPreviewTexture() | 设置预览纹理 |
上面已经介绍了Camera的常用方法,下面根据这些方法详细讲解Android下使用Camera开发拍照应用最基本的过程:
- 使用open()方法获取一个Camera对象,鉴于Android设备可能配置了多个摄像头,open()方法可以通过摄像头Id开启指定的摄像头。
- 为Camera对象设置预览类,它是一个SurfaceHolder对象,通过setPreviewDisplay(SurfaceHolder)方法设置。
- 调用startPreview()方法开始Camera对象的预览。
- 调用takePicture()方法进行拍照,其中可以通过Camera.PictureCallback()回调获得拍摄的Image数据。
- 当拍摄完成后,需要调用stopPreview()方法停止预览,并使用release()释放Camera占用的资源。