前言
在开发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){
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开发拍照应用最基本的过程:
- 使用open()方法获取一个Camera对象,鉴于Android设备可能配置了多个摄像头,open()方法可以通过摄像头Id开启指定的摄像头。
- 为Camera对象设置预览类,它是一个SurfaceHolder对象,通过setPreviewDisplay(SurfaceHolder)方法设置。
- 调用startPreview()方法开始Camera对象的预览。
- 调用takePicture()方法进行拍照,其中可以通过Camera.PictureCallback()回调获得拍摄的Image数据。
- 当拍摄完成后,需要调用stopPreview()方法停止预览,并使用release()释放Camera占用的资源。