人脸识别 --可以正脸扫描获取人脸相关信息

This commit is contained in:
jjLv 2025-03-25 18:29:02 +08:00
parent bc480343d8
commit cc94edb2fe
18 changed files with 953 additions and 185 deletions

View File

@ -7,7 +7,6 @@ if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) {
android {
compileSdk 30
defaultConfig {
applicationId "org.easydarwin.easypusher"
minSdk 29

View File

@ -62,9 +62,9 @@ public class SplashActivity extends BaseSplashActivity implements CancelAdapt {
private void loginOrGoMainPage() {
if (TokenUtils.hasToken()) {
ActivityUtils.startActivity(InitializationActivity.class);
ActivityUtils.startActivity(OperationActivity.class);
} else {
ActivityUtils.startActivity(InitializationActivity.class);
ActivityUtils.startActivity(OperationActivity.class);
}
finish();
}

View File

@ -1,4 +1,4 @@
package com.bonus.canteen.face.util;
package com.bonus.canteen.face.entity;
public class FaceRegisterInfo {
private byte[] featureData;

View File

@ -1,4 +1,4 @@
package com.bonus.canteen.face.util;
package com.bonus.canteen.face.model;
import android.graphics.Rect;
@ -10,7 +10,7 @@ public class DrawInfo {
private int color;
private String name = null;
public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name) {
public DrawInfo(Rect rect, int sex, int age,int liveness,int color,String name) {
this.rect = rect;
this.sex = sex;
this.age = age;

View File

@ -21,7 +21,7 @@ import java.util.List;
*/
public class CameraHelper implements Camera.PreviewCallback {
private static final String TAG = "CameraHelper";
private static Camera mCamera;
private Camera mCamera;
private int mCameraId;
private Point previewViewSize;
private View previewDisplayView;
@ -98,7 +98,6 @@ public class CameraHelper implements Camera.PreviewCallback {
previewSize = getBestSupportedSize(supportedPreviewSizes, previewViewSize);
}
parameters.setPreviewSize(previewSize.width, previewSize.height);
//对焦模式设置
List<String> supportedFocusModes = parameters.getSupportedFocusModes();
if (supportedFocusModes != null && supportedFocusModes.size() > 0) {
@ -244,14 +243,14 @@ public class CameraHelper implements Camera.PreviewCallback {
return bestSize;
}
public static List<Camera.Size> getSupportedPreviewSizes() {
public List<Camera.Size> getSupportedPreviewSizes() {
if (mCamera == null) {
return null;
}
return mCamera.getParameters().getSupportedPreviewSizes();
}
public static List<Camera.Size> getSupportedPictureSizes() {
public List<Camera.Size> getSupportedPictureSizes() {
if (mCamera == null) {
return null;
}

View File

@ -37,5 +37,4 @@ public interface CameraListener {
* @param displayOrientation 相机旋转方向
*/
void onCameraConfigurationChanged(int cameraID, int displayOrientation);
}

View File

@ -9,7 +9,6 @@ public class ConfigUtil {
private static final String APP_NAME = "ArcFaceDemo";
private static final String TRACKED_FACE_COUNT = "trackedFaceCount";
private static final String FT_ORIENT = "ftOrientPriority";
private static final String MAC_PRIORITY = "macPriority";
public static boolean setTrackedFaceCount(Context context, int trackedFaceCount) {
if (context == null) {

View File

@ -8,12 +8,13 @@ import android.hardware.Camera;
import com.arcsoft.face.AgeInfo;
import com.arcsoft.face.GenderInfo;
import com.arcsoft.face.LivenessInfo;
import com.bonus.canteen.face.model.DrawInfo;
import com.bonus.canteen.face.widget.FaceRectView;
import java.util.List;
/**
* 绘制人脸框帮助类用于在{@link FaceRectView}上绘制矩形
* 绘制人脸框帮助类用于在{@link com.bonus.canteen.face.widget.FaceRectView}上绘制矩形
*/
public class DrawHelper {
private int previewWidth, previewHeight, canvasWidth, canvasHeight, cameraDisplayOrientation, cameraId;
@ -211,12 +212,9 @@ public class DrawHelper {
if (drawInfo.getName() == null) {
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setTextSize(rect.width() / 8);
String str = (drawInfo.getSex() == GenderInfo.MALE ? "MALE" : (drawInfo.getSex() == GenderInfo.FEMALE ? "FEMALE" : "UNKNOWN"))
+ ","
+ (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : drawInfo.getAge())
+ ","
+ (drawInfo.getLiveness() == LivenessInfo.ALIVE ? "ALIVE" : (drawInfo.getLiveness() == LivenessInfo.NOT_ALIVE ? "NOT_ALIVE" : "UNKNOWN"));
+ (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : drawInfo.getAge());
canvas.drawText(str, rect.left, rect.top - 10, paint);
} else {
paint.setStyle(Paint.Style.FILL_AND_STROKE);
@ -296,5 +294,4 @@ public class DrawHelper {
public void setMirrorVertical(boolean mirrorVertical) {
this.mirrorVertical = mirrorVertical;
}
}

View File

@ -28,4 +28,11 @@ public class FacePreviewInfo {
this.trackId = trackId;
}
@Override
public String toString() {
return "FacePreviewInfo{" +
"faceInfo=" + faceInfo +
", trackId=" + trackId +
'}';
}
}

View File

@ -0,0 +1,65 @@
package com.bonus.canteen.face.util;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import org.easydarwin.easypusher.R;
import java.util.List;
public class FaceSearchResultAdapter extends RecyclerView.Adapter<FaceSearchResultAdapter.CompareResultHolder> {
private List<CompareResult> compareResultList;
private LayoutInflater inflater;
public FaceSearchResultAdapter(List<CompareResult> compareResultList, Context context) {
inflater = LayoutInflater.from(context);
this.compareResultList = compareResultList;
}
@NonNull
@Override
public CompareResultHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = inflater.inflate(R.layout.recycler_item_search_result, null, false);
CompareResultHolder compareResultHolder = new CompareResultHolder(itemView);
compareResultHolder.textView = itemView.findViewById(R.id.tv_item_name);
compareResultHolder.imageView = itemView.findViewById(R.id.iv_item_head_img);
return compareResultHolder;
}
@Override
public void onBindViewHolder(@NonNull CompareResultHolder holder, int position) {
if (compareResultList == null) {
return;
}
// File imgFile = new File(FaceServer.ROOT_PATH + File.separator + FaceServer.SAVE_IMG_DIR + File.separator + compareResultList.get(position).getUserName() + FaceServer.IMG_SUFFIX);
String path = compareResultList.get(position).getFacePath();
Glide.with(holder.imageView)
.load(path)
.into(holder.imageView);
holder.textView.setText(compareResultList.get(position).getUserName());
}
@Override
public int getItemCount() {
return compareResultList == null ? 0 : compareResultList.size();
}
class CompareResultHolder extends RecyclerView.ViewHolder {
TextView textView;
ImageView imageView;
CompareResultHolder(@NonNull View itemView) {
super(itemView);
}
}
}

View File

@ -20,6 +20,7 @@ import com.bonus.canteen.adapter.menu.utils.StringHelper;
import com.bonus.canteen.adapter.menu.utils.WorkConfig;
import com.bonus.canteen.face.UserFeatureManager;
import com.bonus.canteen.face.entity.BasePersonBean;
import com.bonus.canteen.face.entity.FaceRegisterInfo;
import java.io.File;
import java.util.ArrayList;

View File

@ -0,0 +1,22 @@
package com.bonus.canteen.face.util;
import android.graphics.Color;
/**
* 识别过程中人脸框的颜色
*/
public class RecognizeColor {
/**
* 未知情况的颜色
*/
public static final int COLOR_UNKNOWN = Color.YELLOW;
/**
* 成功的颜色
*/
public static final int COLOR_SUCCESS = Color.GREEN;
/**
* 失败的颜色
*/
public static final int COLOR_FAILED = Color.RED;
}

View File

@ -1,11 +1,16 @@
package com.bonus.canteen.face.util;
package com.bonus.canteen.face.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.bonus.canteen.face.model.DrawInfo;
import com.bonus.canteen.face.util.DrawHelper;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

View File

@ -17,41 +17,68 @@
package com.bonus.canteen.presentation;
import static com.arcsoft.face.enums.DetectFaceOrientPriority.ASF_OP_ALL_OUT;
import android.Manifest;
import android.app.Presentation;
import android.content.Context;
import android.graphics.Point;
import android.hardware.Camera;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceView;
import android.view.Surface;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ListView;
import androidx.annotation.Nullable;
import com.arcsoft.face.AgeInfo;
import com.arcsoft.face.ErrorInfo;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.FaceFeature;
import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.GenderInfo;
import com.arcsoft.face.LivenessInfo;
import com.arcsoft.face.enums.DetectFaceOrientPriority;
import com.arcsoft.face.enums.DetectMode;
import com.bonus.canteen.adapter.menu.PayMenuAdapter;
import com.bonus.canteen.adapter.menu.entity.SalesMenuEntity;
import com.bonus.canteen.face.model.DrawInfo;
import com.bonus.canteen.face.util.CameraHelper;
import com.bonus.canteen.face.util.CameraListener;
import com.bonus.canteen.face.util.CompareResult;
import com.bonus.canteen.face.util.ConfigUtil;
import com.bonus.canteen.face.util.DrawHelper;
import com.bonus.canteen.face.util.FaceHelper;
import com.bonus.canteen.face.util.FaceRectView;
import com.bonus.canteen.face.util.FaceListener;
import com.bonus.canteen.face.util.FacePreviewInfo;
import com.bonus.canteen.face.util.FaceSearchResultAdapter;
import com.bonus.canteen.face.util.FaceServer;
import com.bonus.canteen.face.util.LivenessType;
import com.bonus.canteen.face.util.RecognizeColor;
import com.bonus.canteen.face.util.RequestFeatureStatus;
import com.bonus.canteen.face.util.RequestLivenessStatus;
import com.bonus.canteen.face.widget.FaceRectView;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import com.xuexiang.xui.utils.XToastUtils;
import org.easydarwin.easypusher.R;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
public class MyPresentation extends Presentation {
@ -60,41 +87,35 @@ public class MyPresentation extends Presentation {
private Context context;
private Display display;
/**
* VIDEO模式人脸检测引擎用于预览帧人脸追踪
*/
private FaceEngine ftEngine;
/**
* 用于特征提取的引擎
*/
private FaceEngine frEngine;
/**
* IMAGE模式活体检测引擎用于预览帧人脸活体检测
*/
private FaceEngine flEngine;
List<SalesMenuEntity> salesMenuEntityList = new ArrayList<>();
PayMenuAdapter menuAdapter;
private int ftInitCode = -1;
private int frInitCode = -1;
private int flInitCode = -1;
ListView mListView;
private static final int MAX_DETECT_NUM = 10;
private CameraHelper cameraHelper;
private DrawHelper drawHelper;
private Camera.Size previewSize;
private Integer rgbCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
private int afCode = -1;
private int processMask = FaceEngine.ASF_AGE | FaceEngine.ASF_FACE3DANGLE | FaceEngine.ASF_GENDER | FaceEngine.ASF_LIVENESS;
/**
* 当FR成功活体未成功时FR等待活体的时间
* 相机预览显示的控件可为SurfaceView或TextureView
*/
private static final int WAIT_LIVENESS_INTERVAL = 100;
private View previewView;
private FaceRectView faceRectView;
/**
* 失败重试间隔时间ms
* 所需的所有权限信息
*/
private static final long FAIL_RETRY_INTERVAL = 1000;
/**
* 出错重试最大次数
*/
private static final int MAX_RETRY_TIME = 3;
private List<CompareResult> compareResultList;
private static final String[] NEEDED_PERMISSIONS = new String[]{
Manifest.permission.CAMERA,
Manifest.permission.READ_PHONE_STATE
};
/**
* 用于存储活体值
*/
private ConcurrentHashMap<Integer, Integer> livenessMap = new ConcurrentHashMap<>();
/**
* 活体检测的开关
*/
@ -107,41 +128,45 @@ public class MyPresentation extends Presentation {
* 用于记录人脸特征提取出错重试次数
*/
private ConcurrentHashMap<Integer, Integer> extractErrorRetryMap = new ConcurrentHashMap<>();
private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable();
private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable();
/**
* 用于存储活体检测出错重试次数
*/
private ConcurrentHashMap<Integer, Integer> livenessErrorRetryMap = new ConcurrentHashMap<>();
/**
* 当FR成功活体未成功时FR等待活体的时间
*/
private static final int WAIT_LIVENESS_INTERVAL = 100;
/**
* 失败重试间隔时间ms
*/
private static final long FAIL_RETRY_INTERVAL = 1000;
/**
* 出错重试最大次数
*/
private static final int MAX_RETRY_TIME = 3;
/**
* VIDEO模式人脸检测引擎用于预览帧人脸追踪
*/
private FaceEngine ftEngine;
/**
* 用于特征提取的引擎
*/
private FaceEngine frEngine;
/**
* IMAGE模式活体检测引擎用于预览帧人脸活体检测
*/
private FaceEngine flEngine;
private int ftInitCode = -1;
private int frInitCode = -1;
private int flInitCode = -1;
private static final int MAX_DETECT_NUM = 10;
private FaceHelper faceHelper;
private Camera.Size previewSize;
private CameraHelper cameraHelper;
private DrawHelper drawHelper;
/**
* 相机预览显示的控件可为SurfaceView或TextureView
*/
private View previewView;
/**
* 绘制人脸框的控件
*/
private FaceRectView faceRectView;
/**
* 优先打开的摄像头本界面主要用于单目RGB摄像头设备因此默认打开前置
*/
private Integer rgbCameraID = Camera.CameraInfo.CAMERA_FACING_BACK;
/**
* 识别阈值
*/
private static final float SIMILAR_THRESHOLD = 0.8F;
private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable();
List<SalesMenuEntity> salesMenuEntityList = new ArrayList<>();
PayMenuAdapter menuAdapter;
ListView mListView;
private SurfaceView surfaceView;
private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable();
private List<CompareResult> compareResultList;
private FaceSearchResultAdapter adapter;
public void setSalesData(List<SalesMenuEntity> rawData) {
salesMenuEntityList = new ArrayList<>(rawData);
menuAdapter.notifyDataSetChanged();
@ -159,38 +184,139 @@ public class MyPresentation extends Presentation {
}
}
}
// 获取副屏旋转方向
public int getMyPresentationRotation() {
if (display != null) {
return display.getRotation();
}
return Surface.ROTATION_0; // 默认值
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
setContentView(R.layout.activity_canteen_payment);
initView();
initEngine();
} catch (Exception e) {
e.printStackTrace();
setContentView(R.layout.activity_canteen_payment);
FaceServer.getInstance().loadFace(context);
// 设置副屏窗口常亮
Window window = getWindow();
if (window != null) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
initView();
checkPermissions(NEEDED_PERMISSIONS);
}
private void initView() {
try {
Log.d(TAG, "initView: ");
if (cameraHelper != null) {
cameraHelper.release();
cameraHelper = null;
}
unInitEngine();
} catch (Exception e) {
Log.e(TAG, "initView: 销毁失败 或者 无需销毁");
}
mListView = findViewById(R.id.product_list);
salesMenuEntityList = new ArrayList<>();
menuAdapter = new PayMenuAdapter(context, salesMenuEntityList);
mListView.setAdapter(menuAdapter);
menuAdapter.notifyDataSetChanged();
previewView = findViewById(R.id.texture_preview);
faceRectView = findViewById(R.id.face_rect_view);
surfaceView = findViewById(R.id.face_pay);
}
private void checkPermissions(String[] neededPermissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
XXPermissions.with(context)
.permission(Permission.READ_PHONE_STATE)
.permission(Permission.CAMERA)
.request(new OnPermissionCallback() {
@Override
public void onGranted(List<String> permissions, boolean all) {
if (all) {
XToastUtils.success("获取权限成功");
initModular();
}
}
@Override
public void onDenied(List<String> permissions, boolean never) {
if (never) {
XToastUtils.error("被永久拒绝授权,请手动授予存储权限");
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(context, permissions);
} else {
XToastUtils.error("获取存储权限失败");
}
}
});
} else {
XXPermissions.with(context)
.permission(Permission.READ_PHONE_STATE)
.permission(Permission.CAMERA)
.request(new OnPermissionCallback() {
@Override
public void onGranted(List<String> permissions, boolean all) {
if (all) {
XToastUtils.success("获取权限成功");
initModular();
}
}
@Override
public void onDenied(List<String> permissions, boolean never) {
if (never) {
XToastUtils.error("被永久拒绝授权,请手动授予存储权限");
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(context, permissions);
} else {
XToastUtils.error("获取存储权限失败");
}
}
});
}
}
public static final String APP_ID = "52XE2dQBtdmMsfDMvyKmPCCPyFsc4jvo8TKvAdaYfr28";
public static final String SDK_KEY = "9YFPa6eiuNQAFnzJUadn4LaR8w1bcw3a5ZWYZB6FB57Y";
public void initModular() {
new Thread(new Runnable() {
@Override
public void run() {
activeFaceEngine();
}
}).start();
}
private void activeFaceEngine() {
int code = FaceEngine.activeOnline(context, APP_ID, SDK_KEY);
if (code == ErrorInfo.MOK) {
Log.i(TAG, "activeOnline success");
initEngine();
initCamera();
// if (cameraHelper != null) {
// boolean success = cameraHelper.switchCamera();
// if (!success) {
// Log.e(TAG, "切换失败");
// } else {
// Log.i(TAG, "切换成功");
// }
// } else {
// Log.e(TAG, "cameraHelper 为空");
// }
} else if (code == ErrorInfo.MERR_ASF_ALREADY_ACTIVATED) {
Log.i(TAG, "already activated");
} else {
Log.i(TAG, "activeOnline failed, code is : " + code);
}
}
/**
* 初始化引擎
*/
private void initEngine() {
ConfigUtil.setFtOrient(context, ASF_OP_ALL_OUT);
ftEngine = new FaceEngine();
ftInitCode = ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context),
ftInitCode = ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, DetectFaceOrientPriority.ASF_OP_0_ONLY,
16, MAX_DETECT_NUM, FaceEngine.ASF_FACE_DETECT);
frEngine = new FaceEngine();
@ -201,24 +327,458 @@ public class MyPresentation extends Presentation {
flInitCode = flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY,
16, MAX_DETECT_NUM, FaceEngine.ASF_LIVENESS);
Log.i(TAG, "initEngine: init: " + ftInitCode);
Log.i(TAG, "initEngine: ftInitCode: " + ftInitCode);
Log.i(TAG, "initEngine: frInitCode: " + frInitCode);
Log.i(TAG, "initEngine: flInitCode: " + flInitCode);
if (ftInitCode != ErrorInfo.MOK) {
String error = "初始化失败,错误码为:" + ftInitCode;
String error = "初始化失败,错误码为:"+ ftInitCode;
Log.i(TAG, "initEngine: " + error);
XToastUtils.error(error);
}
if (frInitCode != ErrorInfo.MOK) {
String error = "初始化失败,错误码为:" + frInitCode;
String error = "初始化失败,错误码为:"+ frInitCode;
Log.i(TAG, "initEngine: " + error);
XToastUtils.error(error);
}
if (flInitCode != ErrorInfo.MOK) {
String error = "初始化失败,错误码为:" + flInitCode;
String error = "初始化失败,错误码为:"+ flInitCode;
Log.i(TAG, "initEngine: " + error);
XToastUtils.error(error);
}
}
private void unInitEngine() {
if (afCode == 0) {
afCode = ftEngine.unInit();
afCode = frEngine.unInit();
afCode = flEngine.unInit();
Log.i(TAG, "unInitEngine: " + afCode);
}
}
private void initCamera() {
Log.i(TAG, "initCamera: 开始初始化");
final FaceListener faceListener = new FaceListener() {
@Override
public void onFail(Exception e) {
Log.e(TAG, "onFail: " + e.getMessage());
}
@Override
public void onFaceFeatureInfoGet(@Nullable byte[] nv21Data, @Nullable final FaceFeature faceFeature, Integer requestId, Integer errorCode, FaceInfo faceInfo) {
if (faceFeature != null) {
Log.i(TAG, "onPreview: fr end = " + System.currentTimeMillis() + " trackId = " + requestId);
Integer liveness = livenessMap.get(requestId);
Log.e(TAG, "liveness " +liveness);
if (!livenessDetect) {
//不做活体检测的情况直接搜索
// searchFace(faceFeature, requestId);
Log.i(TAG, "不做活体检测的情况,直接搜索");
} else if (liveness != null && liveness == LivenessInfo.ALIVE) {
//活体检测通过搜索特征
// searchFace(faceFeature, requestId);
Log.i(TAG, "活体检测通过,搜索特征");
} else {
Log.i(TAG, "活体检测未出结果,或者非活体,延迟执行该函数");
//活体检测未出结果或者非活体延迟执行该函数
if (requestFeatureStatusMap.containsKey(requestId)) {
Log.i(TAG, "活体检测未出结果或者非活体延迟执行该函数requestFeatureStatusMap.containsKey(requestId)");
Observable.timer(WAIT_LIVENESS_INTERVAL, TimeUnit.MILLISECONDS)
.subscribe(new Observer<Long>() {
Disposable disposable;
@Override
public void onSubscribe(Disposable d) {
Log.i(TAG, "活体检测未出结果或者非活体延迟执行该函数onSubscribe");
disposable = d;
getFeatureDelayedDisposables.add(disposable);
}
@Override
public void onNext(Long aLong) {
Log.i(TAG, "活体检测未出结果或者非活体延迟执行该函数onNext");
onFaceFeatureInfoGet(nv21Data,faceFeature, requestId, errorCode,faceInfo);
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "onFaceFeatureInfoGet onError: fr error");
}
@Override
public void onComplete() {
Log.i(TAG, "活体检测未出结果或者非活体延迟执行该函数onComplete");
getFeatureDelayedDisposables.remove(disposable);
}
});
}
}
} else {
//特征提取失败
Log.e(TAG, "onFaceFeatureInfoGet: fr error 特征提取失败");
if (increaseAndGetValue(extractErrorRetryMap, requestId) > MAX_RETRY_TIME) {
Log.e(TAG, "onFaceFeatureInfoGet: fr error 特征提取失败次数超过最大次数");
extractErrorRetryMap.put(requestId, 0);
String msg;
// 传入的FaceInfo在指定的图像上无法解析人脸此处使用的是RGB人脸数据一般是人脸模糊
if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) {
msg = "人脸置信度低";
} else {
msg = "ExtractCode:" + errorCode;
}
Log.e(TAG, "onFaceFeatureInfoGet: fr error msg" + msg);
faceHelper.setName(requestId, "未通过:" + msg);
// 在尝试最大次数后特征提取仍然失败则认为识别未通过
requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED);
retryRecognizeDelayed(requestId);
} else {
Log.e(TAG, "onFaceFeatureInfoGet: fr error 特征提取失败次数未超过最大次数");
requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY);
}
}
}
@Override
public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, final Integer requestId, Integer errorCode) {
if (livenessInfo != null) {
int liveness = livenessInfo.getLiveness();
Log.e(TAG, "onFaceLivenessInfoGet: liveness " + liveness);
livenessMap.put(requestId, liveness);
// 非活体重试
if (liveness == LivenessInfo.NOT_ALIVE) {
Log.e(TAG, "onFaceLivenessInfoGet: liveness NOT_ALIVE" + requestId);
faceHelper.setName(requestId, "未通过NOT_ALIVE");
// 延迟 FAIL_RETRY_INTERVAL 将该人脸状态置为UNKNOWN帧回调处理时会重新进行活体检测
retryLivenessDetectDelayed(requestId);
}
} else {
if (increaseAndGetValue(livenessErrorRetryMap, requestId) > MAX_RETRY_TIME) {
Log.e(TAG, "onFaceLivenessInfoGet: liveness error LivenessInfo.NOT_ALIVE");
livenessErrorRetryMap.put(requestId, 0);
String msg;
// 传入的FaceInfo在指定的图像上无法解析人脸此处使用的是RGB人脸数据一般是人脸模糊
if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) {
msg = "人脸置信度低";
} else {
msg = "ProcessCode:" + errorCode;
}
Log.e(TAG, "onFaceLivenessInfoGet: liveness error msg" + msg);
faceHelper.setName(requestId, "未通过:" + msg);
retryLivenessDetectDelayed(requestId);
} else {
Log.e(TAG, "onFaceLivenessInfoGet: liveness error LivenessInfo.NOT_ALIVE");
livenessMap.put(requestId, LivenessInfo.UNKNOWN);
}
}
}
};
CameraListener cameraListener = new CameraListener() {
@Override
public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) {
Log.i(TAG, "onCameraOpened: " + cameraId + " " + displayOrientation + " " + isMirror);
Camera.Size lastPreviewSize = previewSize;
previewSize = camera.getParameters().getPreviewSize();
drawHelper = new DrawHelper(previewSize.width, previewSize.height, previewView.getWidth(), previewView.getHeight(), displayOrientation
, cameraId, isMirror, false, false);
// 切换相机的时候可能会导致预览尺寸发生变化
if (faceHelper == null ||
lastPreviewSize == null ||
lastPreviewSize.width != previewSize.width || lastPreviewSize.height != previewSize.height) {
Log.i(TAG, "onCameraOpened: previewSize changed");
faceHelper = new FaceHelper.Builder()
.ftEngine(ftEngine)
.frEngine(frEngine)
.flEngine(flEngine)
.frQueueSize(MAX_DETECT_NUM)
.flQueueSize(MAX_DETECT_NUM)
.previewSize(previewSize)
.faceListener(faceListener)
.trackedFaceCount(ConfigUtil.getTrackedFaceCount(context))
.build();
Log.i(TAG, "onCameraOpened: trackedFaceCount jieshu");
}else{
Log.i(TAG, "onCameraOpened: trackedFaceCount 切换相机的时候可能会导致预览尺寸发生变化" );
}
}
@Override
public void onPreview(byte[] nv21, Camera camera) {
if (faceRectView != null) {
faceRectView.clearFaceInfo();
}
List<FacePreviewInfo> facePreviewInfoList = faceHelper.onPreviewFrame(nv21);
if (facePreviewInfoList != null && faceRectView != null && drawHelper != null) {
drawPreviewInfo(facePreviewInfoList);
}
clearLeftFace(facePreviewInfoList);
Log.i(TAG, "onPreview: " + facePreviewInfoList.size());
if (facePreviewInfoList != null && facePreviewInfoList.size() > 0 && previewSize != null) {
for (int i = 0; i < facePreviewInfoList.size(); i++) {
Integer status = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId());
Log.e(TAG, "onPreview: status " + status);
/**
* 在活体检测开启在人脸识别状态不为成功或人脸活体状态不为处理中ANALYZING且不为处理完成ALIVENOT_ALIVE时重新进行活体检测
*/
if (livenessDetect && (status == null || status != RequestFeatureStatus.SUCCEED)) {
Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId());
Log.e(TAG, "onPreview: liveness " + liveness);
if (liveness == null
|| (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING)) {
livenessMap.put(facePreviewInfoList.get(i).getTrackId(), RequestLivenessStatus.ANALYZING);
faceHelper.requestFaceLiveness(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId(), LivenessType.RGB);
}
}
/**
* 对于每个人脸若状态为空或者为失败则请求特征提取可根据需要添加其他判断以限制特征提取次数
* 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传
*/
if (status == null
|| status == RequestFeatureStatus.TO_RETRY) {
requestFeatureStatusMap.put(facePreviewInfoList.get(i).getTrackId(), RequestFeatureStatus.SEARCHING);
faceHelper.requestFaceFeature(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId());
}
}
}
// List<FaceInfo> faceInfoList = new ArrayList<>();
//
// int code = ftEngine.detectFaces(nv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList);
// Log.i(TAG, " faceInfoList.szie" + faceInfoList.size());
// if (code == ErrorInfo.MOK && faceInfoList.size() > 0) {
// code = ftEngine.process(nv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList, processMask);
// if (code != ErrorInfo.MOK) {
// return;
// }
// } else {
// return;
// }
// Log.i(TAG, "faceEngine.process:" + code);
// List<AgeInfo> ageInfoList = new ArrayList<>();
// List<GenderInfo> genderInfoList = new ArrayList<>();
// List<Face3DAngle> face3DAngleList = new ArrayList<>();
// List<LivenessInfo> faceLivenessInfoList = new ArrayList<>();
// int ageCode = ftEngine.getAge(ageInfoList);
// int genderCode = ftEngine.getGender(genderInfoList);
// int face3DAngleCode = ftEngine.getFace3DAngle(face3DAngleList);
// int livenessCode = ftEngine.getLiveness(faceLivenessInfoList);
// Log.i(TAG, "onPreview: " + ageCode + " " + genderCode + " " + face3DAngleCode + " " + livenessCode);
// // 有其中一个的错误码不为ErrorInfo.MOKreturn
// if ((ageCode | genderCode | face3DAngleCode | livenessCode) != ErrorInfo.MOK) {
// return;
// }
// if (faceRectView != null && drawHelper != null) {
// List<DrawInfo> drawInfoList = new ArrayList<>();
// for (int i = 0; i < faceInfoList.size(); i++) {
// drawInfoList.add(new DrawInfo(drawHelper.adjustRect(faceInfoList.get(i).getRect()), genderInfoList.get(i).getGender(), ageInfoList.get(i).getAge(), faceLivenessInfoList.get(i).getLiveness(), RecognizeColor.COLOR_UNKNOWN, null));
// }
// drawHelper.draw(faceRectView, drawInfoList);
// }
}
@Override
public void onCameraClosed() {
Log.i(TAG, "onCameraClosed: ");
}
@Override
public void onCameraError(Exception e) {
Log.i(TAG, "onCameraError: " + e.getMessage());
}
@Override
public void onCameraConfigurationChanged(int cameraID, int displayOrientation) {
if (drawHelper != null) {
drawHelper.setCameraDisplayOrientation(displayOrientation);
}
Log.i(TAG, "onCameraConfigurationChanged: " + cameraID + " " + displayOrientation);
}
};
cameraHelper = new CameraHelper.Builder()
.previewViewSize(new Point(previewView.getMeasuredWidth(), previewView.getMeasuredHeight()))
.rotation(getMyPresentationRotation())
.isMirror(false)
.previewOn(previewView)
.cameraListener(cameraListener)
.specificCameraId(0)
.build();
cameraHelper.init();
cameraHelper.start();
}
/**
* 将map中key对应的value增1回传
*
* @param countMap map
* @param key key
* @return 增1后的value
*/
public int increaseAndGetValue(Map<Integer, Integer> countMap, int key) {
if (countMap == null) {
return 0;
}
Integer value = countMap.get(key);
if (value == null) {
value = 0;
}
countMap.put(key, ++value);
return value;
}
/**
* 延迟 FAIL_RETRY_INTERVAL 重新进行活体检测
*
* @param requestId 人脸ID
*/
private void retryLivenessDetectDelayed(final Integer requestId) {
Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS)
.subscribe(new Observer<Long>() {
Disposable disposable;
@Override
public void onSubscribe(Disposable d) {
disposable = d;
delayFaceTaskCompositeDisposable.add(disposable);
}
@Override
public void onNext(Long aLong) {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
// 将该人脸状态置为UNKNOWN帧回调处理时会重新进行活体检测
if (livenessDetect) {
faceHelper.setName(requestId, Integer.toString(requestId));
}
livenessMap.put(requestId, LivenessInfo.UNKNOWN);
delayFaceTaskCompositeDisposable.remove(disposable);
}
});
}
/**
* 延迟 FAIL_RETRY_INTERVAL 重新进行人脸识别
*
* @param requestId 人脸ID
*/
private void retryRecognizeDelayed(final Integer requestId) {
requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED);
Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS)
.subscribe(new Observer<Long>() {
Disposable disposable;
@Override
public void onSubscribe(Disposable d) {
disposable = d;
delayFaceTaskCompositeDisposable.add(disposable);
}
@Override
public void onNext(Long aLong) {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
// 将该人脸特征提取状态置为FAILED帧回调处理时会重新进行活体检测
faceHelper.setName(requestId, Integer.toString(requestId));
requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY);
delayFaceTaskCompositeDisposable.remove(disposable);
}
});
}
private void drawPreviewInfo(List<FacePreviewInfo> facePreviewInfoList) {
List<DrawInfo> drawInfoList = new ArrayList<>();
for (int i = 0; i < facePreviewInfoList.size(); i++) {
String name = faceHelper.getName(facePreviewInfoList.get(i).getTrackId());
Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId());
Integer recognizeStatus = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId());
// 根据识别结果和活体结果设置颜色
int color = RecognizeColor.COLOR_UNKNOWN;
if (recognizeStatus != null) {
if (recognizeStatus == RequestFeatureStatus.FAILED) {
color = RecognizeColor.COLOR_FAILED;
}
if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
color = RecognizeColor.COLOR_SUCCESS;
}
}
if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) {
color = RecognizeColor.COLOR_FAILED;
}
drawInfoList.add(new DrawInfo(drawHelper.adjustRect(facePreviewInfoList.get(i).getFaceInfo().getRect()),
GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness == null ? LivenessInfo.UNKNOWN : liveness, color,
name == null ? String.valueOf(facePreviewInfoList.get(i).getTrackId()) : name));
}
drawHelper.draw(faceRectView, drawInfoList);
}
/**
* 删除已经离开的人脸
*
* @param facePreviewInfoList 人脸和trackId列表
*/
private void clearLeftFace(List<FacePreviewInfo> facePreviewInfoList) {
if (compareResultList != null) {
for (int i = compareResultList.size() - 1; i >= 0; i--) {
if (!requestFeatureStatusMap.containsKey(compareResultList.get(i).getTrackId())) {
compareResultList.remove(i);
adapter.notifyItemRemoved(i);
}
}
}
if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) {
requestFeatureStatusMap.clear();
livenessMap.clear();
livenessErrorRetryMap.clear();
extractErrorRetryMap.clear();
if (getFeatureDelayedDisposables != null) {
getFeatureDelayedDisposables.clear();
}
return;
}
Enumeration<Integer> keys = requestFeatureStatusMap.keys();
while (keys.hasMoreElements()) {
int key = keys.nextElement();
boolean contained = false;
for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
if (facePreviewInfo.getTrackId() == key) {
contained = true;
break;
}
}
if (!contained) {
requestFeatureStatusMap.remove(key);
livenessMap.remove(key);
livenessErrorRetryMap.remove(key);
extractErrorRetryMap.remove(key);
}
}
}
}

View File

@ -0,0 +1,13 @@
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/scanning">
<shape android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#11E1EF"
android:dashWidth="8dp"
android:dashGap="8dp"/>
<corners android:radius="12dp"/>
</shape>
</item>
<!-- 添加动画过渡效果 -->
</animated-selector>

View File

@ -0,0 +1,19 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 全屏半透明背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#80000000" />
</shape>
</item>
<!-- 居中挖空区域 -->
<item
android:width="250dp"
android:height="250dp"
android:gravity="center">
<shape android:shape="rectangle">
<solid android:color="#E43333" />
<corners android:radius="12dp" />
</shape>
</item>
</layer-list>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2025 xuexiangjys(xuexiangjys@163.com)
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@ -21,49 +20,51 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
android:baselineAligned="false">
<!-- 左侧商品明细 -->
<LinearLayout
android:id="@+id/left_panel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1"
android:orientation="vertical"
android:padding="16dp">
<!-- 菜品明细标题 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center"
android:background="@drawable/bg_radius_top_10"
android:backgroundTint="#F1E9E9"
android:layout_gravity="center">
android:backgroundTint="#F1E9E9">
<ImageView
android:layout_width="30dp"
android:layout_gravity="center"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:src="@drawable/ic_menu_meal"/>
android:src="@drawable/ic_menu_meal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textFontWeight="1000"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:text="菜品明细"
android:textFontWeight="1000"
android:textSize="18sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<include layout="@layout/canteen_food_pay_list" />
<include layout="@layout/canteen_food_pay_list" />
</LinearLayout>
</LinearLayout>
<!-- 总计金额 -->
<LinearLayout
android:layout_width="match_parent"
@ -74,43 +75,48 @@
android:padding="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/white"
android:text="总数量1份"
android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/white"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="总数量1份"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/white" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="合计8.00元"
android:gravity="center"
android:text="合计8.00元"
android:textColor="@color/white"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="优惠0.00元"
android:gravity="center"
android:text="优惠0.00元"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
@ -118,18 +124,21 @@
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/white"/>
android:background="@color/white" />
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="应付:"
android:textColor="@color/white"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -143,52 +152,60 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="5dp">
android:layout_marginTop="5dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_radius_top_10"
android:orientation="vertical"
tools:ignore="UselessParent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/bg_radius_top_10"
android:backgroundTint="#F1E9E9">
<ImageView
android:layout_width="30dp"
android:layout_gravity="center"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:src="@drawable/ic_nutrition"/>
android:src="@drawable/ic_nutrition" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:text="营养摄入"
android:textFontWeight="1000"
android:layout_marginStart="10dp"
android:textSize="18sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0"
android:textFontWeight="500"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -200,16 +217,18 @@
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0"
android:textFontWeight="500"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -218,18 +237,21 @@
android:textFontWeight="500"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0"
android:textFontWeight="500"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -238,18 +260,21 @@
android:textFontWeight="500"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0"
android:textFontWeight="500"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -258,18 +283,21 @@
android:textFontWeight="500"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0"
android:textFontWeight="500"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -290,9 +318,9 @@
android:id="@+id/right_panel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:padding="50dp"
android:layout_weight="1">
android:padding="20dp">
<!-- 扫描框 -->
<androidx.constraintlayout.widget.ConstraintLayout
@ -302,32 +330,40 @@
<!-- 支付提示 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="100dp"
android:elevation="666dp"
android:gravity="center"
android:padding="16dp"
android:text="请支付 8.00 元"
android:elevation="666dp"
android:textSize="24sp"
android:textColor="#fff"
android:textSize="28sp"
tools:ignore="MissingConstraints" />
<SurfaceView
android:id="@+id/face_pay"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/face_rect_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/texture_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<com.bonus.canteen.face.widget.FaceRectView
android:id="@+id/face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<!-- 支付方式 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#80000000"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:padding="16dp"
tools:ignore="MissingConstraints">
<!-- 扫码支付 -->
@ -344,13 +380,26 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_qrcode_scan" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:textColor="#FFF"
android:text="扫码支付\n请将二维码对准扫码口"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:text="扫码支付"
android:textColor="#FFF"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:text="请将二维码对准扫码口"
android:textColor="#FFF"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<!-- 刷卡支付 -->
@ -366,14 +415,26 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_card_scan" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:text="刷卡支付\n请将卡片对准感应区"
android:textColor="#FFF"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:text="刷卡支付"
android:textColor="#FFF"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:text="请将卡片对准感应区"
android:textColor="#FFF"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<ImageView
android:id="@+id/iv_item_head_img"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/tv_item_name"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:background="#80000000"
android:gravity="center"
android:textColor="@android:color/white" />
</LinearLayout>