引言
最近项目中需要做一个不启动预览界面,自动完成拍照的功能(嘿嘿,不要误会我没有做什么坏事),其实是正常的人脸识别功能,机器人自动扫描当有人来的时候,自动走上正前方迎宾并播放欢迎词,由于这一功能是无限循环的耗时且消耗资源,自然得放到子线程中,才不会影响主线程之间的人机交互。于是就有了一个小小小的较通用点的小模板——可以应用于简单常驻任务的监控(一直运行于APP的生命周期)吧,分享下希望大家给点改进建议。
一、功能及模块概述
1、自动对焦并拍照
我们知道系统为我们提供了拍照Activity,但是这样的话就启动了预览界面,而且无法在不影响交互的情况下拍照,所以只能是自己写利用Camera(也可以用Camera2这个高版本中的类),由于实时刷新的所以SurfaceView可以用作是预览的取景界面,再有一个就是自动对焦,如果不自动对焦的话就会出现拍出来的文字很模糊根本看不清。
//自动对焦
camera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
if(success){
initCamera();//实现相机的参数初始化
camera.cancelAutoFocus();//只有加上了这一句,才会自动对焦。
}
}
});
2、全周期驻活监控并自动识别
前面也说了这是个耗时且无限循环的工作,明显不可能写在主线程中,但是如果直接写在主线程里的话可能容易被杀死,所以我选择的是白色驻活手段(具体的看下面的代码)基本能满足我的需求,写在自定义的Service里,并封装其他相关回调接口(主要用于调用Activity里的方法)。
3、同步请求
由于人脸识别过程可以分为两个步骤:拍照,拍照成功之后上传到服务器进行识别,这里我采用的是同步请求,因为采用异步的话就有可能导致上一条请求还没响应,新的又发送了,这根本没有必要,采用同步正是基于此,完成了一个人脸识别流程之后才进行下一个流程,如此循环,所以必须严格控制业务流程,先拍照成功之后才能上传,返回响应结果之后,再根据结果进行新的识别流程。
二、实现步骤
1、首先定义业务类,主要用于完成拍照和HTTP请求,分别传递的是一个CallBack回调类,用于在相应业务执行完毕之后触发下一业务的执行。
package crazymo.train.notifyacititydo.biz;
import static crazymo.train.notifyacititydo.biz.DetectService.listener;
/**
* auther: CrazyMo_
* Date: 2017/2/24
* Time:10:48
* Des:
*/
public class DetectFaceBiz {
///这里定义了自定义的CallBack类,原打算用于监听在拍照完成之后执行相关操作
public void takePictrue(DetectService.OnFinishTakePicCallBack callBack){
boolean istakePic=listener.onTakePhoto();
/*if(istakePic) {
Log.e("DetectFace","已结成功拍照");
callBack.onTakePicFinish();
}else{
Log.e("DetectFace","TakePictrue failed");
}*/
}
public void startDetect(DetectService.OnFinishDetectCallBack callBack){
///发起网络请求
//do some network
callBack.onDetectFinish();
}
}
2、核心监控业务流程类
package crazymo.train.notifyacititydo.biz;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
/**
* auther: CrazyMo_
* Date: 2017/2/24
* Time:10:24
* Des:
*/
public class DetectService extends Service {
private DetectThread detectThread = null;
public final static DetectFaceBiz biz = new DetectFaceBiz();
private Context context;
private Looper looper;
private DetectHandler handler;
public static OnTakePhotoListener listener;
public DetectService(Context context) {
//在构造方法里初始化并启动线程
super();
this.context = context;
HandlerThread thread = new HandlerThread("LedControlService");
thread.start();
looper = context.getMainLooper();
this.handler = new DetectHandler(looper);
this.detectThread = new DetectThread();
detectThread.start();
}
/**
* 职责是用于改变线程的其他状态机,这里没有写出来。。
*/
private class DetectHandler extends Handler {
public DetectHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
DetectThread.setBizTag(msg.what);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;//START_STICKY:如果service进程被kill掉,保留service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试重新创建service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand(Intent,int,int)方法。如果在此期间没有任何启动命令被传递到service,那么参数Intent将为null。
}
@Override
public void onDestroy() {
super.onDestroy();
}
/**
* 启动服务
*/
public void start() {
if (context != null) {
Intent service = new Intent(context, DetectService.class);
context.startService(service);
}
}
/**
* 用于设置挂起或开始DetectThread 的正常工作
* @param flag true 正常工作 false 挂起
*/
public void setDetectingState(boolean flag){
detectThread.setDetectState(flag);
}
public static class DetectThread extends Thread {
public static boolean needDetect = false;
public static int bizTag = 1;//用于标识业务类型的
@Override
public void run() {
while (!(Thread.currentThread().isInterrupted())) {
synchronized (this) {
while (!needDetect) {
try {
Log.e("DetectFace","DetectThread 启动并挂起ing");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
switch (bizTag) {
case 1:
Log.e("DetectFace","预准备开始拍照流程");
biz.takePictrue(new OnFinishTakePicCallBack() {
@Override
public void onTakePicFinish() {
///拍照成功之后再调检测 bizTag=2;
Log.e("DetectFace","已经拍照成功带上传到Face++");
setBizTag(2);
}
});
break;
case 2:
Log.e("DetectFace","预准备开始检测流程");
biz.startDetect(new OnFinishDetectCallBack() {
@Override
public void onDetectFinish() {
///检测成功之后,根据结果再拍照进行下一轮的脸部识别 bizTag=1
Log.e("DetectFace","已经检测成功,待重新拍照进行下一次检测流程");
setBizTag(1);
}
});
break;
case -1:
default:
break;
}
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void setDetectState(boolean isNeed) {
needDetect = isNeed;
if (needDetect){
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized static void setBizTag(int tag) {
bizTag = tag;
}
}
public void setOnDetectedListener(OnTakePhotoListener listener) {
this.listener = listener;
}
/**
* 用于给其他业务发送Handler消息
*
* @param msg
*/
public void sendMessage(@NonNull int msg) {
if (handler != null) {
handler.sendEmptyMessage(msg);
}
}
//用于调用Activity的方法而提供的回调接口
public interface OnTakePhotoListener {
boolean onTakePhoto();
}
//拍照成功之后触发的回调
public interface OnFinishTakePicCallBack {
void onTakePicFinish();
}
//上传服务器并返回响应结果时触发的回调
public interface OnFinishDetectCallBack {
void onDetectFinish();
}
}
3、在Application中初始化
public class DetectApplication extends Application {
private static DetectService detectService;
@Override
public void onCreate() {
super.onCreate();
init();
}
private void init(){
if(detectService==null) {
detectService = new DetectService(this);
}
detectService.start();
}
public static DetectService getDetectService(){
return detectService;
}
}
5、配置相关权限
尤其注意当targetSdkVersion大于等于24时,需要动态申请权限
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CAMERA" />
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<!-- 往SDCard写入数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
5、在MainActivity里实现
package crazymo.train.notifyacititydo;
import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import crazymo.train.notifyacititydo.biz.DetectService;
public class MainActivity extends AppCompatActivity implements DetectService.OnTakePhotoListener, View.OnClickListener {
private DetectService service;
private SurfaceView surfaceView;
private Button takePickBtn;
private Camera camera;
private Camera.Parameters parameters = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init(){
getView();
initCamera();
service=DetectApplication.getDetectService();
Log.e("DetectFace","setOnDetectedListener 正常工作ing");
service.setOnDetectedListener(MainActivity.this);
service.setDetectingState(true);//开启Detect流程
Log.e("DetectFace","setDetectState true 正常工作ing");
}
private void getView(){
surfaceView= (SurfaceView) findViewById(R.id.camera_surfv);
takePickBtn= (Button) findViewById(R.id.take_pic_btn);
takePickBtn.setOnClickListener(this);
}
private void initCamera(){
WindowManager manager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);//获取WM对象
DisplayMetrics dm = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(dm);
surfaceView.getHolder()
.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
surfaceView.getHolder().setFixedSize(dm.widthPixels, dm.heightPixels); //设置Surface分辨率
surfaceView.getHolder().setSizeFromLayout();
surfaceView.getHolder().setKeepScreenOn(true);// 屏幕常亮
surfaceView.getHolder().addCallback(new SurfaceCallback());//为SurfaceView的句柄添加一个回调函数
}
@Override
public boolean onTakePhoto() {
takePhoto();
return true;
}
@Override
public void onClick(View v) {
if(v.getId()==R.id.take_pic_btn){
if (camera != null) {
// 拍照
camera.takePicture(null, null, new MyPictureCallback());
}
}
}
public void takePhoto(){
if (camera != null) {
// 拍照
camera.takePicture(null, null, new MyPictureCallback());
}
}
private final class MyPictureCallback implements Camera.PictureCallback {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
try {
// bundle = new Bundle();
//bundle.putByteArray("bytes", data); //将图片字节数据保存在bundle当中,实现数据交换
saveToSDCard(data); // 保存图片到sd卡中
Toast.makeText(getApplicationContext(), "Take photo Success",
Toast.LENGTH_SHORT).show();
DetectService.DetectThread.setBizTag(2);
camera.startPreview(); // 拍完照后,重新开始预览
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 将拍下来的照片存放在SD卡中
* @param data
* @throws IOException
*/
public static void saveToSDCard(byte[] data) throws IOException {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss"); // 格式化时间
String filename = format.format(date) + ".jpg";
File fileFolder = new File(Environment.getExternalStorageDirectory()
+ "/rebot/cache/");
if (!fileFolder.exists()) { // 如果目录不存在,则创建一个名为"finger"的目录
fileFolder.mkdir();
}
File jpgFile = new File(fileFolder, filename);
FileOutputStream outputStream = new FileOutputStream(jpgFile); // 文件输出流
outputStream.write(data); // 写入sd卡中
outputStream.close(); // 关闭输出流
}
private final class SurfaceCallback implements SurfaceHolder.Callback {
// 拍照状态变化时调用该方法
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
//实现自动对焦
if(camera!=null) {
parameters = camera.getParameters(); // 获取各项参数
parameters.setPreviewSize(width, height); // 设置预览大小
// 设置预览照片时每秒显示多少帧的最小值和最大值
parameters.setPreviewFpsRange(4, 10);
parameters.setPictureFormat(ImageFormat.JPEG); // 设置图片格式
parameters.set("jpeg-quality", 100); // 设置JPG照片的质量
parameters.setPictureSize(width, height); // 设置保存的图片尺寸
}
}
// 开始拍照时调用该方法
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
camera = Camera.open(); // 打开摄像头
camera.setPreviewDisplay(holder); // 设置用于显示拍照影像的SurfaceHolder对象
camera.setDisplayOrientation(getPreviewDegree(MainActivity.this));
//自动对焦
camera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
if(success){
initCamera();//实现相机的参数初始化
camera.cancelAutoFocus();//只有加上了这一句,才会自动对焦。
}
}
});
camera.startPreview(); // 开始预览
} catch (Exception e) {
e.printStackTrace();
}
}
// 停止拍照时调用该方法
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (camera != null) {
camera.release(); // 释放照相机
camera = null;
}
}
}
/**
* 提供一个静态方法,用于根据手机方向获得相机预览画面旋转的角度
*/
public static int getPreviewDegree(Activity activity) {
// 获得手机的方向
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degree = 0;
// 根据手机的方向计算相机预览画面应该选择的角度
switch (rotation) {
case Surface.ROTATION_0:
degree = 90;
break;
case Surface.ROTATION_90:
degree = 0;
break;
case Surface.ROTATION_180:
degree = 270;
break;
case Surface.ROTATION_270:
degree = 180;
break;
}
return degree;
}
@Override
protected void onStop() {
super.onStop();
service.setDetectingState(false);
}
@Override
protected void onDestroy() {
super.onDestroy();
service.setDetectingState(false);
DetectService.DetectThread.setBizTag(-1);
}
}
activity_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<SurfaceView
android:id="@+id/camera_surfv"
android:layout_width="1dp"
android:layout_height="1dp"
/>
<Button
android:id="@+id/take_pic_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="拍照"/>
</LinearLayout>