一、Camera
-
Camera 用于捕获图像和视频
-
在 Android 开发的早期阶段,Android 提供
android.hardware.Camera
API,开发者用它来访问和控制设备的摄像头硬件 -
然而,随着 Android 系统的发展,从 Android 5.0(API 级别 21)开始,Android 引入了一个新的 Camera2 API,以提供更强大和灵活的控制功能
二、Camera 拍照
1、Util
- MyCameraTool.java
package com.my.camera.util;
import android.hardware.Camera;
import android.util.Log;
import com.my.camera.entity.CameraIdResult;
import java.util.List;
public class MyCameraTool {
public static final String TAG = MyCameraTool.class.getSimpleName();
/**
* 得到摄像头 Id 对象
*
* @return 摄像头 Id 对象,它有前置摄像头 Id 和后置摄像头 Id
*/
public static CameraIdResult getCameraIdResult() {
CameraIdResult cameraIdResult = new CameraIdResult();
int numberOfCameras = Camera.getNumberOfCameras();
Log.i(TAG, "------------------------------ 摄像头个数:" + numberOfCameras);
for (int cameraId = 0; cameraId < numberOfCameras; cameraId++) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
Log.i(TAG, "------------------------------ 前置摄像头,cameraId 为:" + cameraId);
cameraIdResult.setQzCameraId(cameraId);
}
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
Log.i(TAG, "------------------------------ 后置摄像头,cameraId 为:" + cameraId);
cameraIdResult.setHzCameraId(cameraId);
} else {
Log.i(TAG, "------------------------------ 其他摄像头,cameraId 为:" + cameraId);
}
}
return cameraIdResult;
}
/**
* 根据 SurfaceView 的尺寸和相机支持的预览尺寸来选择最优的预览尺寸
*
* @param sizes 相机支持的预览尺寸列表
* @param w SurfaceView 的宽度
* @param h SurfaceView 的高度
* @return 最优的预览尺寸,如果没有合适的尺寸则返回 null
*/
public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
if (sizes == null) return null;
final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) h / w;
Camera.Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
int targetHeight = h;
// 遍历所有支持的预览尺寸
for (Camera.Size size : sizes) {
// 检查宽高比是否接近目标宽高比
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
// 计算当前尺寸与目标尺寸的宽度差异
// 如果差异小于当前最小差异,则更新最优尺寸和最小差异
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
// 如果找不到接近目标宽高比的尺寸,则选择最接近目标高度的尺寸
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
/**
* 打印相机参数信息
*
* @param parameters 相机参数对象
*/
public static void showParameters(Camera.Parameters parameters) {
Camera.Size previewSize = parameters.getPreviewSize();
Log.i(TAG, "============================== getPreviewSize");
Log.i(TAG, "---------- " + previewSize.width + " - " + previewSize.height);
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
Log.i(TAG, "============================== supportedPreviewSizes");
Log.i(TAG, "============================== supportedPreviewSizes size: " + supportedPreviewSizes.size());
for (Camera.Size supportedPreviewSize : supportedPreviewSizes) {
Log.i(TAG, "---------- " + supportedPreviewSize.width + " - " + supportedPreviewSize.height);
}
Camera.Size pictureSize = parameters.getPictureSize();
Log.i(TAG, "============================== getPictureSize");
Log.i(TAG, "---------- " + pictureSize.width + " - " + pictureSize.height);
List<Camera.Size> supportedPictureSizes = parameters.getSupportedPictureSizes();
Log.i(TAG, "============================== getSupportedPictureSizes");
Log.i(TAG, "============================== getSupportedPictureSizes: " + supportedPictureSizes.size());
for (Camera.Size supportedPictureSize : supportedPictureSizes) {
Log.i(TAG, "---------- " + supportedPictureSize.width + " - " + supportedPictureSize.height);
}
String focusMode = parameters.getFocusMode();
Log.i(TAG, "============================== getFocusMode");
Log.i(TAG,"---------- " + focusMode);
String whiteBalance = parameters.getWhiteBalance();
Log.i(TAG, "============================== getWhiteBalance");
Log.i(TAG,"---------- " + whiteBalance);
int exposureCompensation = parameters.getExposureCompensation();
Log.i(TAG, "============================== getExposureCompensation");
Log.i(TAG,"---------- " + exposureCompensation);
String flashMode = parameters.getFlashMode();
Log.i(TAG, "============================== getFlashMode");
Log.i(TAG,"---------- " + flashMode);
}
}
- MyIOTool.java
package com.my.camera.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* IO 工具类
*
*/
public class MyIOTool {
/**
* 根据指定目录路径创建目录(支持多级目录)
*
* @param dirPath 目录路径,如 d:/logs
*/
public static void createDirectory(String dirPath) {
File fileDir = new File(dirPath);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
}
/**
* 根据指定文件路径创建文件
* 如果文件所在目录未创建,则会自动创建
*
* @param filePath 文件路径,如 d:/logs/myLogs.log
*/
public static void createFile(String filePath) {
File file = new File(filePath);
if (!file.exists()) {
createDirectory(file.getParent());
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 根据指定文件路径删除文件
*
* @param filePath 文件路径
*/
public static void deleteFile(String filePath) {
File file = new File(filePath);
file.delete();
}
/**
* 根据指定文件路径创建文件,若文件存在则覆盖
*
* @param filePath 文件路径
*/
public static void overwriteFile(String filePath) {
deleteFile(filePath);
createFile(filePath);
}
/**
* 写入文件
*
* @param filePath 文件路径
* @param data 待写入的数据
* @param isAppend 是否追加,不是就是覆盖
*/
public static void writeFile(String filePath, byte[] data, boolean isAppend) {
File file = new File(filePath);
if (!file.exists()) {
// 如果文件不存在,则创建文件
createFile(filePath);
} else {
// 如果文件存在,且指定的是覆盖模式,则覆盖创建文件
if (!isAppend) overwriteFile(filePath);
}
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file, isAppend);
fileOutputStream.write(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 覆盖写入文件
*
* @param filePath 文件路径
* @param data 待写入的数据
*/
public static void writeFile(String filePath, byte[] data) {
writeFile(filePath, data, false);
}
}
2、Activity Layout
- activity_take_photo.java
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TakePhotoActivity"
tools:ignore="MissingConstraints">
<SurfaceView
android:id="@+id/sv"
android:layout_width="300dp"
android:layout_height="300dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_take_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
3、Activity Code
- TakePhotoActivity.java
package com.my.camera;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.Button;
import com.my.camera.entity.CameraIdResult;
import com.my.camera.util.MyCameraTool;
import com.my.camera.util.MyIOTool;
import com.my.camera.util.MyImageTool;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
@SuppressWarnings("all")
public class TakePhotoActivity extends AppCompatActivity {
public static final String TAG = TakePhotoActivity.class.getSimpleName();
private SurfaceView sv;
private Button btnTakePhoto;
private SurfaceHolder surfaceHolder;
private int hzCameraId;
private Camera camera;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_take_photo);
// 保持屏幕常亮
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
CameraIdResult cameraIdResult = MyCameraTool.getCameraIdResult();
hzCameraId = cameraIdResult.getHzCameraId();
sv = findViewById(R.id.sv);
btnTakePhoto = findViewById(R.id.btn_take_photo);
surfaceHolder = sv.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.i(TAG, "------------------------------ surfaceCreated");
openCamera();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
Log.i(TAG, "------------------------------ surfaceChanged");
cameraPreview(width, height);
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
Log.i(TAG, "------------------------------ surfaceDestroyed");
closeCamera();
}
});
btnTakePhoto.setOnClickListener(v -> takePhoto());
}
private void openCamera() {
try {
if (camera != null) return;
if (hzCameraId == -1) return;
camera = Camera.open(hzCameraId);
camera.setPreviewDisplay(surfaceHolder);
camera.setDisplayOrientation(90);
} catch (IOException e) {
e.printStackTrace();
closeCamera();
}
}
private void cameraPreview(int width, int height) {
if (camera == null) return;
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
Camera.Size optimalPreviewSize = MyCameraTool.getOptimalPreviewSize(supportedPreviewSizes, width, height);
Log.i(TAG, "============================== optimalPreviewSize");
Log.i(TAG, "---------- " + optimalPreviewSize.width + " - " + optimalPreviewSize.height);
parameters.setPreviewSize(optimalPreviewSize.width, optimalPreviewSize.height);
parameters.setPictureSize(optimalPreviewSize.width, optimalPreviewSize.height);
parameters.setRotation(90);
camera.setParameters(parameters);
camera.startPreview();
}
private void takePhoto() {
if (camera == null) return;
camera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
closeCamera();
Log.i(TAG, "------------------------------ 得到照片数据 size 为 " + data.length);
String photoPath = getFilesDir().getPath() + File.separatorChar + UUID.randomUUID().toString() + ".png";
MyIOTool.writeFile(photoPath, data);
}
});
}
private void closeCamera() {
if (camera == null) return;
camera.stopPreview();
camera.release();
camera = null;
}
}
三、Camera 拍照案例解析
1、添加权限
- 在 AndroidManifest.xml 文件中添加相关权限,如果是 Android 6.0(API 级别 23)或之后,需要在运行时请求
<uses-permission android:name="android.permission.CAMERA" />
2、保持屏幕常亮
- 保持屏幕常亮就是不会自动关闭屏幕,不仅是预览图像,它也非常适用于阅读、视频播放等
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
3、选择摄像头
- 这里借助工具类,选择后置摄像头 ID,准备使用后置摄像头(手机背后的)
CameraIdResult cameraIdResult = MyCameraTool.getCameraIdResult();
hzCameraId = cameraIdResult.getHzCameraId();
4、准备 SurfaceView 作为图像预览容器
- SurfaceView 控件可用于预览图像、视频播放等
<SurfaceView
android:id="@+id/sv"
android:layout_width="300dp"
android:layout_height="300dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
- 通过 SurfaceView 对象得到 SurfaceHolder 对象,它作为预览图像显示,它有三个方法
-
surfaceCreated(@NonNull SurfaceHolder holder)
:当 Surface 被创建时调用,这里可以打开相机 -
surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height)
:当 Surface 的尺寸或格式发生变化时调用,这里可以调整相机预览的尺寸并开始预览 -
surfaceDestroyed(@NonNull SurfaceHolder holder)
:当 Surface 被销毁时调用,这里可以关闭相机
sv = findViewById(R.id.sv);
surfaceHolder = sv.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.i(TAG, "------------------------------ surfaceCreated");
openCamera();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
Log.i(TAG, "------------------------------ surfaceChanged");
cameraPreview(width, height);
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
Log.i(TAG, "------------------------------ surfaceDestroyed");
closeCamera();
}
});
5、打开相机
-
camera = Camera.open(hzCameraId);
:通过打开摄像头 -
camera.setPreviewDisplay(surfaceHolder);
:设置预览图像显示 -
camera.setDisplayOrientation(90);
:设置预览方向(否则图像不是正向的)
private void openCamera() {
try {
if (camera != null) return;
if (hzCameraId == -1) return;
camera = Camera.open(hzCameraId);
camera.setPreviewDisplay(surfaceHolder);
camera.setDisplayOrientation(90);
} catch (IOException e) {
e.printStackTrace();
closeCamera();
}
}
6、调整相机预览的尺寸并开始预览
-
Camera.Parameters parameters = camera.getParameters();
:得到相机参数对象 -
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
:得到支持的预览尺寸列表 -
Camera.Size optimalPreviewSize = MyCameraTool.getOptimalPreviewSize(supportedPreviewSizes, width, height);
:借助工具类选择最优的预览尺寸 -
parameters.setPreviewSize(optimalPreviewSize.width, optimalPreviewSize.height);
:设置图像预览的尺寸 -
parameters.setPictureSize(optimalPreviewSize.width, optimalPreviewSize.height);
:设置拍照照片的尺寸 -
parameters.setRotation(90);
:设置照片方向(否则照片不是正向的) -
camera.startPreview();
:开始预览图像
private void cameraPreview(int width, int height) {
if (camera == null) return;
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
Camera.Size optimalPreviewSize = MyCameraTool.getOptimalPreviewSize(supportedPreviewSizes, width, height);
Log.i(TAG, "============================== optimalPreviewSize");
Log.i(TAG, "---------- " + optimalPreviewSize.width + " - " + optimalPreviewSize.height);
parameters.setPreviewSize(optimalPreviewSize.width, optimalPreviewSize.height);
parameters.setPictureSize(optimalPreviewSize.width, optimalPreviewSize.height);
parameters.setRotation(90);
camera.setParameters(parameters);
camera.startPreview();
}
7、拍照
-
通过调用 takePicture 方法来拍照
-
该方法接收 3 个参数,其中 2 个被设置为 null,即不需要关注拍照过程中的快门声和 JPEG 回调
-
第 3 个参数是一个
Camera.PictureCallback
实例对象,用于处理拍照完成后获得的照片数据,这里借助工具类将照片存储到私有空间的 files 目录下
private void takePhoto() {
if (camera == null) return;
camera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
closeCamera();
Log.i(TAG, "------------------------------ 得到照片数据 size 为 " + data.length);
String photoPath = getFilesDir().getPath() + File.separatorChar + UUID.randomUUID().toString() + ".png";
MyIOTool.writeFile(photoPath, data);
}
});
}
8、关闭相机
-
camera.stopPreview();
:停止预览图像 -
camera.release();
:释放摄像头资源
private void closeCamera() {
if (camera == null) return;
camera.stopPreview();
camera.release();
camera = null;
}