Android Camera、Camera2详解
前言
Android5.0之前使用android.hardware包下的Camera类进行拍照、录视频等功能。5.0以后,新增了android.hardware.camera2包,利用新的机制、新的类进行拍照、录视频。
camera使用
摄像头权限自己去AndroidMainfest.xml配置就行了
直接上代码工具类
下面展示一些
代码
。
package com.tony.sonicinspection.ui.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.WindowManager;
import com.serenegiant.widget.Camera2Helper;
import com.tony.sonicinspection.App;
import com.tony.sonicinspection.Const;
import com.tony.sonicinspection.bean.eventbean.CameraPicPathEventBean;
import com.tony.sonicinspection.utils.ImageUtil;
import org.greenrobot.eventbus.EventBus;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
/**
* **************************************************************************************************
* 修改日期 功能或Bug描述 作者
* 2021/08/02 TextureView类目 ljh
* **************************************************************************************************
*/
public class MyTextureView extends TextureView implements View.OnLayoutChangeListener {
public Camera mCamera;
private Context context;
private Camera.Parameters param;
private boolean isCanTakePicture = false;
int mWidth = 0;
int mHeight = 0;
int mDisplayWidth = 0;
int mDisplayHeight = 0;
int mPreviewWidth = 540;
int mPreviewHeight = 399;
public MyTextureView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
private void init() {
if (null == mCamera) {
mCamera = Camera.open();
}
this.setSurfaceTextureListener(new SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
param = mCamera.getParameters();
param.setPreviewFpsRange(4,10);
param.getPictureSize().width = mPreviewWidth;
param.getPictureSize().height = mPreviewHeight;
//param.setPictureSize(mPreviewWidth,mPreviewHeight);这么写会报错,有想法的可以去查查代码学习学习
param.setPictureFormat(PixelFormat.JPEG);
param.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
param.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);// 1连续对焦
mCamera.setParameters(param);
//变形处理
RectF previewRect = new RectF(0, 0, mPreviewWidth, mPreviewHeight);
RectF surfaceDimensions = new RectF(0, 0, mPreviewWidth, 960);
Matrix matrix = new Matrix();
matrix.setRectToRect(previewRect, surfaceDimensions, Matrix.ScaleToFit.FILL);
MyTextureView.this.setTransform(matrix);
//<-处理变形 处理旋转
int displayRotation = 0;
WindowManager windowManager = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
int rotation = windowManager.getDefaultDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_0:
displayRotation = 0;
break;
case Surface.ROTATION_90:
displayRotation = 90;
break;
case Surface.ROTATION_180:
displayRotation = 180;
break;
case Surface.ROTATION_270:
displayRotation = 270;
break;
}
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(0, info);
//设置方向
int orientation;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
orientation = (info.orientation - displayRotation + 360) % 360;
} else {
orientation = (info.orientation + displayRotation) % 360;
orientation = (360 - orientation) % 360;
}
mCamera.setParameters(param);
mCamera.setDisplayOrientation(orientation);
try {
mCamera.setPreviewTexture(surfaceTexture);
mCamera.startPreview();
isCanTakePicture = true;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
isCanTakePicture = true;
}
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
}
});
}
/**
* 拍照
*/
public void take() {
if (mCamera != null && isCanTakePicture) {
// isCanTakePicture = false;
mCamera.takePicture(new Camera.ShutterCallback() {
@Override
public void onShutter() {
}
}, null, mPictureCallback);
}
}
public void startPreview() {
if (mCamera != null && !isCanTakePicture) {
MyTextureView.this.setBackgroundDrawable(null);
mCamera.startPreview();
isCanTakePicture = true;
}
}
public void stopPreview() {
if (mCamera != null) {
mCamera.stopPreview();
isCanTakePicture = false;
}
}
public void releaseTextureView(){
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
isCanTakePicture = true;
}
}
Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
if (mCamera != null) {
try {
/**这里是为了展示照片做的处理,可根据自己的需要去做相应操作
EventBus.getDefault().post(new CameraPicPathEventBean(data));*/
Const.PHOTO_PATH = App.getInstance().generatePhotoPath();
File file = new File(Const.PHOTO_PATH);
file.createNewFile();
FileOutputStream os = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(os);
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
bitmap = ImageUtil.rotaingImageView(90, bitmap);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos);
bos.flush();
bos.close();
os.close();
// MyTextureView.this.setBackgroundDrawable(new BitmapDrawable(bitmap));
mCamera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
mWidth = right - left;
mHeight = bottom - top;
}
}
Activity使用 ,这我是我项目中的一些代码 ,根据自己需要自行处理逻辑
@BindView(R.id.textureView_system_camera)
MyTextureView textureViewSystemCamera;
//开启相机预览
textureViewSystemCamera.startPreview();
//关闭相机预览
textureViewSystemCamera.stopPreview();
@Subscribe(threadMode = ThreadMode.MAIN)
public void showPic(CameraPicPathEventBean cameraPicPathEventBean) {
if (cameraPicPathEventBean != null) {
showDialog(cameraPicPathEventBean.getData());
}
}
//初始化并弹出对话框方法
private void showDialog(byte[] data) {
View view = LayoutInflater.from(getActivity()).inflate(R.layout.popup_capture, null, false);
final AlertDialog dialog = new AlertDialog.Builder(getActivity()).setView(view).create();
TextView delete = (TextView) view.findViewById(R.id.tv_delete);
TextView save = (TextView) view.findViewById(R.id.tv_save);
// Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
// bitmap = ImageUtil.rotaingImageView(90, bitmap);//图片旋转
Glide.with(this)
.load(data)
.into((ImageView) view.findViewById(R.id.captureImage));
delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
File file = new File(Const.PHOTO_PATH);
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri uri = Uri.fromFile(file);
intent.setData(uri);
getActivity().sendBroadcast(intent);
file.delete();
mainActivity.showToast(R.string.device_socket_capture_delete_success);
//... To-do
dialog.dismiss();
}
});
save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//... To-do
mainActivity.showToast("保存成功:" + Const.PHOTO_PATH);
curInspectionData = null;
curImageIndex = 0;
CommonUtils.updateGallery(mainActivity, Const.PHOTO_PATH);
dialog.dismiss();
}
});
dialog.show();
//此处设置位置窗体大小,我这里设置为了手机屏幕宽度的3/4 注意一定要在show方法调用后再写设置窗口大小的代码,否则不起效果会
dialog.getWindow().setLayout((getActivity().getResources().getDisplayMetrics().widthPixels/4*3),LinearLayout.LayoutParams.WRAP_CONTENT);
}
camera2使用
直接上使用方法
@BindView(R.id.surfaceView_system_camera)
SurfaceView surfaceViewSystemCamera;
private SurfaceHolder mSurfaceHolder;
private CameraManager cameraManager;
private String cameraID;//摄像头Id 0 为后 1 为前
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 CameraDevice mCameraDevice;//摄像头
private Handler childHandler, mainHandler;
private ImageReader mImageReader; //摄像头照片
private CameraCaptureSession mCameraCaptureSession;//摄像头预览
/**
* 初始化摄像头view
*/
private void initCameraView() {
mSurfaceHolder = surfaceViewSystemCamera.getHolder();
mSurfaceHolder.setKeepScreenOn(true);
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
initCamera2();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// 释放Camera资源
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
}
});
}
/**
* 初始化摄像头
*/
private void initCamera2() {
HandlerThread handlerThread = new HandlerThread("Camera2");
handlerThread.start();
childHandler = new Handler(handlerThread.getLooper());
mainHandler = new Handler(Looper.getMainLooper());
cameraID = "" + CameraCharacteristics.LENS_FACING_FRONT;
mImageReader = ImageReader.newInstance(540, 399, ImageFormat.JPEG,1);
//变形处理
RectF previewRect = new RectF(0, 0, 540, 399);
RectF surfaceDimensions = new RectF(0, 0, 540, 960);
Matrix matrix = new Matrix();
matrix.setRectToRect(previewRect, surfaceDimensions, Matrix.ScaleToFit.FILL);
surfaceViewSystemCamera.setAnimationMatrix(matrix);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
try {
// 拿到拍照照片数据
Image image = reader.acquireNextImage();
// Image image = reader.acquireLatestImage();
if (image != null) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//由缓冲区存入字节数组
showDialog(bytes);
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
Const.PHOTO_PATH = App.getInstance().generatePhotoPath();
File file = new File(Const.PHOTO_PATH);
file.createNewFile();
FileOutputStream os = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(os);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos);
bos.flush();
bos.close();
os.close();
image.close();//如果频繁使用拍照,必须调用此方法要不然会报错
// java.lang.IllegalStateException: maxImages (1) has already been acquired, call #close before acqu
}
} catch (IOException e) {
e.printStackTrace();
}
}
},mainHandler);
cameraManager = (CameraManager) getActivity().getSystemService(Context.CAMERA_SERVICE);
if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
try {
cameraManager.openCamera(cameraID, stateCallback, mainHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
//开启预览
takePreview();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
Toast.makeText(getActivity(), "摄像头开启失败", Toast.LENGTH_SHORT).show();
}
};
/**
* 预览摄像头视频
*/
private void takePreview() {
// 创建预览需要的CaptureRequest.Builder
try {
final CaptureRequest.Builder previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 将SurfaceView的surface作为CaptureRequest.Builder的目标
previewRequestBuilder.addTarget(mSurfaceHolder.getSurface());
mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceHolder.getSurface(), mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
if (null == mCameraDevice) return;
// 当摄像头已经准备好时,开始显示预览
mCameraCaptureSession = session;
try {
// 自动对焦
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 打开闪光灯
// previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 显示预览
CaptureRequest previewRequest = previewRequestBuilder.build();
mCameraCaptureSession.setRepeatingRequest(previewRequest, null, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Toast.makeText(getActivity(), "配置失败", Toast.LENGTH_SHORT).show();
}
},childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 拍照
*/
private void takePicture() {
if (mCameraDevice == null) return;
// 创建拍照需要的CaptureRequest.Builder
final CaptureRequest.Builder captureRequestBuilder;
try {
captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
// 将imageReader的surface作为CaptureRequest.Builder的目标
captureRequestBuilder.addTarget(mImageReader.getSurface());
// 自动对焦
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 自动曝光
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 获取手机方向
int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
// 根据设备方向计算设置照片的方向
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
//拍照
CaptureRequest mCaptureRequest = captureRequestBuilder.build();
mCameraCaptureSession.capture(mCaptureRequest, null, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
//关闭
if (mCameraDevice != null) {
mCameraDevice.close();
}
//开启
if (mCameraDevice != null) {
initCamera2();
} else {
initCameraView();//初始化一次就可以了,根据自己需要使用
}
1.求最佳比例正方形分辨率
较为歪门邪道的方法,核心就是TextureView/SurfaceView的宽高比与摄像头的高宽比做差值比较,注意这里一个是宽高一个是高宽,求出来的结果就是在指定指定比例最接近正方形的分辨率
优点:因为是正方形的分辨率,所以在预览的时候不管是什么尺寸的TextureView/SurfaceView的都能显示的不会变形.所以比较适合在小尺寸TextureView/SurfaceView上
缺点:在预览的时候其实无法完全显示完整(正方形不管怎么样都有可能上下或者左右超出View的大小),所以TextureView/SurfaceView会自动忽略四周部分,只显示最中间的部分.这样拍照的时候就会发现预览与实际照片显示范围不一致.
/**
* 获取匹配的大小 这里是Camera2获取分辨率数组的方式,Camera1获取不同,计算一样
* @return
*/
private Size getMatchingSize(){
Size selectSize = null;
float selectProportion = 0;
try {
float viewProportion = (float)mTextureView.getWidth() / (float)mTextureView.getHeight();//计算View的宽高比
CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCurrentCameraId);
StreamConfigurationMap streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = streamConfigurationMap.getOutputSizes(ImageFormat.JPEG);
for (int i = 0; i < sizes.length; i++){
Size itemSize = sizes[i];
float itemSizeProportion = (float)itemSize.getHeight() / (float)itemSize.getWidth();//计算当前分辨率的高宽比
float differenceProportion = Math.abs(viewProportion - itemSizeProportion);//求绝对值
Log.e(TAG, "相减差值比例="+differenceProportion );
if (i == 0){
selectSize = itemSize;
selectProportion = differenceProportion;
continue;
}
if (differenceProportion <= selectProportion){ //判断差值是不是比之前的选择的差值更小
if (differenceProportion == selectProportion){ //如果差值与之前选择的差值一样
if (selectSize.getWidth() + selectSize.getHeight() < itemSize.getWidth() + itemSize.getHeight()){//选择分辨率更大的Size
selectSize = itemSize;
selectProportion = differenceProportion;
}
}else {
selectSize = itemSize;
selectProportion = differenceProportion;
}
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
Log.e(TAG, "getMatchingSize: 比例="+selectProportion);
Log.e(TAG, "getMatchingSize: 尺寸,宽="+selectSize.getWidth()+",高="+selectSize.getHeight());
return selectSize;
}
2.求最满足宽度的情况下,在找到最接近高度的分辨率
这个是最正常的算法了,核心就是找到与屏幕宽度最接近的分辨率,然后在找最接近屏幕高度的分辨率.这里是屏幕宽度是最高优先级的,其次在满足高度.
优点:预览图像与拍照照片的效果完全一致.
缺点:
只能满足全屏幕预览的拍照情况下,各种奇葩自定义大小的TextureView你基本上不可能找到满足的宽度的分辨率.
因为需要让TextureView的高度跟随分辨率高度,所以预览的上面或者下面可能会有需要留出空白区域的情况.(可以用黑色背景View填充),全屏预览的时候可以忽略这个情况,因为基本上手机的摄像头都会有一个分辨率刚好与屏幕分辨率一致.但是不排除个别奇葩手机
在看代码前这里说明一个重要知识!摄像头分辨率的宽度和高度与屏幕分辨率的宽度和高度的对应
1.摄像头分辨率的宽度和高度其实是手机横屏下的才是正确方向.如下图所示
2.屏幕的分辨率的宽度和高度依然是手机竖屏下的高度和宽度.
private Size getMatchingSize2(){
Size selectSize = null;
try {
CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCurrentCameraId);
StreamConfigurationMap streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = streamConfigurationMap.getOutputSizes(ImageFormat.JPEG);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); //因为我这里是将预览铺满屏幕,所以直接获取屏幕分辨率
int deviceWidth = displayMetrics.widthPixels; //屏幕分辨率宽
int deviceHeigh = displayMetrics.heightPixels; //屏幕分辨率高
Log.e(TAG, "getMatchingSize2: 屏幕密度宽度="+deviceWidth);
Log.e(TAG, "getMatchingSize2: 屏幕密度高度="+deviceHeigh );
/**
* 循环40次,让宽度范围从最小逐步增加,找到最符合屏幕宽度的分辨率,
* 你要是不放心那就增加循环,肯定会找到一个分辨率,不会出现此方法返回一个null的Size的情况
* ,但是循环越大后获取的分辨率就越不匹配
*/
for (int j = 1; j < 41; j++) {
for (int i = 0; i < sizes.length; i++) { //遍历所有Size
Size itemSize = sizes[i];
Log.e(TAG,"当前itemSize 宽="+itemSize.getWidth()+"高="+itemSize.getHeight());
//判断当前Size高度小于屏幕宽度+j*5 && 判断当前Size高度大于屏幕宽度-j*5
if (itemSize.getHeight() < (deviceWidth + j*5) && itemSize.getHeight() > (deviceWidth - j*5)) {
if (selectSize != null){ //如果之前已经找到一个匹配的宽度
if (Math.abs(deviceHeigh-itemSize.getWidth()) < Math.abs(deviceHeigh - selectSize.getWidth())){ //求绝对值算出最接近设备高度的尺寸
selectSize = itemSize;
continue;
}
}else {
selectSize = itemSize;
}
}
}
/**
* 因为需要满足宽度,所以原则上如果没有必要,首次找到最接近宽度屏幕的摄像头高度后就跳出循环
*/
if(selectSize != null){ Log.e(TAG, "getMatchingSize2: 选择的分辨率宽度="+selectSize.getWidth()); Log.e(TAG, "getMatchingSize2: 选择的分辨率高度="+selectSize.getHeight());
return selectSize;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
Log.e(TAG, "宽="+selectSize.getWidth());
Log.e(TAG, "高="+selectSize.getHeight());
return selectSize;
}
暂时就这么多,具体camera2原理的话:
链接:
https://www.jianshu.com/p/0ea5e201260f