应用中我们时常会遇到要制作拍照上传功能,但我们一般的做法都是直接调起系统的拍照界面或者使用第三方提供的界面。如果用户有定制要求或者我们有自己的一些需求我们该怎么办呢?今天我们按步骤完成一个自定义相机的制作。
1.Camera相关的API
拍照我们主要用到的两个类,一个是SurfaceView,这个我们在上一次就介绍过了;还有一个就是Camera了。所以我们需要了解一下Camera相关API。
- getNumberOfCameras:获取本设备的摄像头数目。
- open:打开摄像头,默认打开后置摄像头。如果有多个摄像头,那么open(0)表示打开后置摄像头,open(1)表示打开前置摄像头。
- getParameters:获取摄像头的拍照参数,返回Camera.Parameters对象。
- setParameters:设置摄像头的拍照参数。具体的拍照参数通过调用Camera.Parameters的下列方法进行设置。
setPreviewSize | 设置预览界面尺寸 |
setPictureSize | 设置保存图片的尺寸。 |
setPictureFormat | 设置图片格式。一般使用ImageFormat.JPEG表示JPG格式。 |
setFocusMode | 设置对焦模式。取值Camera.Parameters.FOCUS_MODE_AUTO只会对焦一次;取值FOCUS_MODE_CONTINUOUS_PICTURE则会连续对焦 |
- setPreviewDisplay:设置预览界面的表面持有者,即SurfaceHolder对象。该方法必须在SurfaceHolder.Callback的surfaceCreated方法中调用。
- startPreview:开始预览。该方法必须在setPreviewDisplay方法之后调用。
- unlock:录像时需要对摄像头解锁,这样摄像头才能持续录像。该方法必须现在startPreview方法之后调用。
- setDisplayOrientation:设置预览的角度。Android的0度在三点钟的水平位置,而手机屏幕是垂直位置,从水平位置到垂直位置需要旋转90度。
- autoFocus:设置对焦事件。参数自动对焦接口AutoFocusCallback的onAutoFocus方法在对焦完成时触发,在此提示用户对焦完毕可以拍照了。
- takePicture:开始拍照,并设置拍照相关事件。第一个参数为快门回调接口ShutterCallback,它的onShutter方法在按下快门时触发,通常可在此播放拍照声音,默认为“咔嚓”一声;第二个参数的PictureCallback表示原始图像的回调接口,通常无须处理直接传null;第三个参数的PictureCallback表示JPG图像的回调接口,压缩后的图像数据可在该接口中的onPictureTaken方法中获得。
- setZoomChangeListener:设置缩放比例变化事件。缩放变化监听器OnZoomChangeListener的onZoomChange方法在缩放比例发生变化时触发。
- setPreviewCallback:设置预览回调事件,通常在连拍时调用。预览回调接口PreviewCallback的onPreviewFrame方法在预览图像发生变化时触发。
- stopPreview:停止预览。
- lock:录像完毕对摄像头加锁。该方法在stopPreview方法之后调用。
- release:释放摄像头。因为摄像头不能重复打开,所以每次退出拍照时都要释放摄像头。
2.代码设置表面视图SurfaceView
接着我们一步一步来实现我们的功能。首先我们要把表面视图做好。
首先我们的布局是这样的。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<Button
android:id="@+id/bt_take_photo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="拍照"/>
</RelativeLayout>
里面一共有两个元素,一个是我们的表面视图SurfaceView,另一个是我们的拍照按钮。
下面是我们Activity的代码
public class MainActivity extends WaterPermissionActivity<MainModel>
implements MainCallback, View.OnClickListener {
private Button bt_take_photo;
private SurfaceView surfaceView;
@Override
protected MainModel getModelImp() {
return new MainModel(this,this);
}
@Override
protected int getContentLayoutId() {
return R.layout.activity_main;
}
@Override
protected void initWidget() {
bt_take_photo = findViewById(R.id.bt_take_photo);
surfaceView = findViewById(R.id.surfaceView);
bt_take_photo.setOnClickListener(this);
//获取表面视图的表面持有者
SurfaceHolder holder = surfaceView.getHolder();
//给表面持有者添加表面变更监听器
holder.addCallback(mSurfaceCallback);
//去除黑色背景,TRANSLUCENT半透明,TRANSPARENT透明
holder.setFormat(PixelFormat.TRANSPARENT);
requestPermission(CAMERA);
}
@Override
protected void doCamera() {
requestPermission(READ_EXTERNAL_STORAGE);
}
@Override
protected void doSDRead() {
requestPermission(WRITE_EXTERNAL_STORAGE);
}
@Override
protected void doSDWrite() {
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.bt_take_photo:
//点击拍照
break;
}
}
/**
* 表面变更监听器
*/
private SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
};
}
MainActivity,MainCallback,MainModel是我的MVC框架结构,不用管他。如果想了解的同学可以看我之前的博客,或者留言问我。代码中还包含了我们请求动态权限的内容,老规矩,大家可以用自己的。
核心代码有两部分,第一部分
//获取表面视图的表面持有者
SurfaceHolder holder = surfaceView.getHolder();
//给表面持有者添加表面变更监听器
holder.addCallback(mSurfaceCallback);
//去除黑色背景,TRANSLUCENT半透明,TRANSPARENT透明
holder.setFormat(PixelFormat.TRANSPARENT);
第二部分就是第一部分中的mSurfaceCallback的定义
private SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
};
清单文件配置的权限也不要忘记
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.FLASHLIGHT"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
到这里,我们的表面视图就设置好了。
3.显示摄像头摄制的画面到SurfaceView上
首先我么引入一个相机操作的工具类,一会儿我们会用到
public class CameraUtil {
private static final Pattern COMMA_PATTERN = Pattern.compile(",");
public static Point getSize(Context ctx) {
WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
Point size = new Point();
size.x = dm.widthPixels;
size.y = dm.heightPixels;
return size;
}
public static Point getCameraSize(Camera.Parameters params, Point screenSize) {
String previewSizeValueString = params.get("preview-size-values");
if (previewSizeValueString == null) {
previewSizeValueString = params.get("preview-size-value");
}
Point cameraSize = null;
if (previewSizeValueString != null) {
cameraSize = findBestPreviewSizeValue(previewSizeValueString, screenSize);
}
if (cameraSize == null) {
cameraSize = new Point((screenSize.x >> 3) << 3, (screenSize.y >> 3) << 3);
}
return cameraSize;
}
private static Point findBestPreviewSizeValue(CharSequence previewSizeValueString, Point screenSize) {
int bestX = 0;
int bestY = 0;
int diff = Integer.MAX_VALUE;
for (String previewSize : COMMA_PATTERN.split(previewSizeValueString)) {
previewSize = previewSize.trim();
int dimPosition = previewSize.indexOf('x');
if (dimPosition < 0) {
continue;
}
int newX;
int newY;
try {
newX = Integer.parseInt(previewSize.substring(0, dimPosition));
newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
} catch (NumberFormatException nfe) {
continue;
}
int newDiff = Math.abs((newX - screenSize.x) + (newY - screenSize.y));
if (newDiff == 0) {
bestX = newX;
bestY = newY;
break;
} else if (newDiff < diff) {
bestX = newX;
bestY = newY;
diff = newDiff;
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}
return null;
}
}
接着我们在刚才的MainActivity中,加入如下成员变量
private Camera mCamera;//声明一个相机对象
private boolean isPreviewing;//是否正在预览
private Point mCameraSize;//相机画面的尺寸
private int mCameraType = 0;//设置前置还是后置 0:后置 1:前置
public static int CAMERA_BEHIND = 0; // 后置摄像头
public static int CAMERA_FRONT = 1; // 前置摄像头
在SurfaceHolder.Callback中我们加入如下代码
/**
* 表面变更监听器
*/
private SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
//打开摄像头
mCamera = Camera.open(mCameraType);
try {
//设置相机的预览页面
mCamera.setPreviewDisplay(holder);
//获得相机画面的尺寸
mCameraSize = CameraUtil.getCameraSize(mCamera.getParameters()
, CameraUtil.getSize(MainActivity.this));
//获取相机的参数信息
Camera.Parameters parameters = mCamera.getParameters();
//设置预览界面的尺寸
parameters.setPreviewSize(1920, 1080);
//设置图片的分辨率
parameters.setPictureSize(1920, 1080);
parameters.setJpegQuality(100); // 设置照片质量
//设置图片的格式
parameters.setPictureFormat(ImageFormat.JPEG);
//设置对焦模式为自动对焦。前置摄像头似乎无法自动对焦
if (mCameraType == CAMERA_BEHIND) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
}
//设置相机的参数信息
mCamera.setParameters(parameters);
} catch (IOException e) {
e.printStackTrace();
mCamera.release();//遇到异常要释放相机资源
mCamera = null;
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
//设置相机的展示角度
mCamera.setDisplayOrientation(90);
//开始预览画面
mCamera.startPreview();
isPreviewing = true;
//开始自动对焦
mCamera.autoFocus(null);
//设置相机的预览监听器。注意这里的setPreviewCallback给连拍功能用
// mCamera.setPreviewCallback(mPr);
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
//将预览监听器置空
mCamera.setPreviewCallback(null);
//停止预览画面
mCamera.stopPreview();
//释放相机资源
mCamera.release();
mCamera = null;
}
};
这样我们运行起来就会发现SurfaceView显示出了我们的图像。
4.拍照
接下来我们就要进入我们的关键功能拍照了。
首先就是我们拍照按钮的点击事件,我们这里让它调用一个方法
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bt_take_photo:
//点击拍照
doTakePicture();
break;
}
}
拍照方法的实现如下
private void doTakePicture(){
if (isPreviewing && mCamera!=null){
//命令相机拍摄一张照片
mCamera.takePicture(mShutterCallback,null,mPictureCallback);
}
}
这里用到了两个对象,mShutterCallback是我们实现的按快门监听对象,mPictureCallback是我们拍照结果的回调。
mShutterCallback的实现
private Camera.ShutterCallback mShutterCallback = new Camera.ShutterCallback() {
@Override
public void onShutter() {
}
};
这个是我们按下快门后的一个监听,我们可以让程序在这里播放一个咔嚓声,或进行其他的一些操作。
mPictureCallback的实现
private Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
Bitmap raw = null;
if (null != data) {
//原始图像数据data是字节数组,需要将其解析成位图
raw = BitmapFactory.decodeByteArray(data, 0, data.length);
//停止预览画面
mCamera.stopPreview();
isPreviewing = false;
}
//旋转位图
Bitmap bitmap = getRotateBitmap(raw
, (mCameraType == CAMERA_BEHIND) ? 90 : -90);
//获取本次拍摄的照片保存路径
List<String> listPath = new ArrayList<>();
listPath.add("myCamera");
listPath.add("photos");
mPhotoPath = PathGetUtil.getLongwayPath(MainActivity.this, listPath);
File fileDir = new File(mPhotoPath);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
File filePic = new File(mPhotoPath, "ww" + System.currentTimeMillis() + ".jpg");
if (!filePic.exists()) {
try {
filePic.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
saveImage(filePic.getPath(), bitmap);
//保存文件需要时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再次进入预览画面
mCamera.startPreview();
isPreviewing = true;
}
};
其中我们使用了一个旋转位图的工具方法,代码如下
// 获得旋转角度之后的位图对象
public static Bitmap getRotateBitmap(Bitmap b, float rotateDegree) {
// 创建操作图片用的矩阵对象
Matrix matrix = new Matrix();
// 执行图片的旋转动作
matrix.postRotate(rotateDegree);
// 创建并返回旋转后的位图对象
return Bitmap.createBitmap(b, 0, 0, b.getWidth(),b.getHeight(), matrix, false);
}
成员变量中我们加入一个照片的存储路径
private String mPhotoPath;//照片的保存路径
操作Bitmap相关方法
public static void saveImage(String path, Bitmap bitmap){
try {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(path));
bitmap.compress(Bitmap.CompressFormat.JPEG,80,bos);
bos.flush();
bos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
这样我们自定义的一个相机功能就完成了。
ps:对于现在的手机,Camera已经可以说过时了。我使用Android 9.0的手机测试,拍摄效果已经不尽如人意。而我们设置的预览尺寸和照片尺寸已经是最大可以支持的1920*1080,所以明显已经不是很适合现在的手机使用。不过作为开发者我们需要对这个技术有一个了解,并且明白基本的一个实现方式。这样对我们之后Camera2和Jetpack中提供的CameraX的学习可以有一个很好的帮助。