Android 开发 Camera类的拍照与录像

时间:2022-02-05 13:51:02

前言    

  在开发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

拍照开发

流程

  1. 获取权限
  2. 初始化曲面视图View(SurfaceView或者TextureView)(用于显示相机预览图像)
  3. 初始化打开相机,选择前后摄像头
  4. 配置相机参数
  5. 拍照
  6. 处理照片返回数据,旋转照片与压缩照片

获取权限

 <!-- 相机相关 -->
<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){
List<Camera.Size> previewSizeList = 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<Camera.Size> 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开发拍照应用最基本的过程:

  1. 使用open()方法获取一个Camera对象,鉴于Android设备可能配置了多个摄像头,open()方法可以通过摄像头Id开启指定的摄像头。
  2. 为Camera对象设置预览类,它是一个SurfaceHolder对象,通过setPreviewDisplay(SurfaceHolder)方法设置。
  3. 调用startPreview()方法开始Camera对象的预览。
  4. 调用takePicture()方法进行拍照,其中可以通过Camera.PictureCallback()回调获得拍摄的Image数据。
  5. 当拍摄完成后,需要调用stopPreview()方法停止预览,并使用release()释放Camera占用的资源。