feat: 新增护眼距离和行走抖动检测提醒功能

This commit is contained in:
2026-07-30 09:36:10 +08:00
parent 132000b4e1
commit 621ebd64d7
25 changed files with 2112 additions and 76 deletions

1
.gitignore vendored
View File

@@ -88,3 +88,4 @@ lint/tmp/
/app/src/androidTest/java/com/jiaoguanyi/appstore/
/adb
/app/proguard-rules.pro
/.codebuddy/

View File

@@ -29,7 +29,7 @@ android {
ndk {
//选择要添加的对应 cpu 类型的 .so 库。
abiFilters 'arm64-v8a'
abiFilters 'arm64-v8a', 'armeabi-v7a'
// 还可以添加 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
}
@@ -736,13 +736,25 @@ dependencies {
implementation files('libs/BaiduTraceSDK_v3_1_10.jar')
// 基础注解库
implementation "androidx.annotation:annotation:1.10.0"
// 包含 OptIn 实验性 Lint 规则检测的核心库
implementation "androidx.annotation:annotation-experimental:1.1.0"
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.lifecycle:lifecycle-service:2.3.1"
// CameraX 核心库及生命周期绑定
def camerax_version = "1.0.0" // 请根据实际情况选择稳定版本
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:1.0.0-alpha23"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -776,7 +788,8 @@ dependencies {
//Google
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.zxing:core:3.5.0'
// implementation 'com.google.mlkit:face-detection:16.1.7'
implementation 'com.google.mlkit:face-detection:16.1.5'
//图片加载框架
implementation 'com.github.bumptech.glide:glide:4.13.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2'

View File

@@ -11,6 +11,10 @@
</intent>
</queries>
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.front" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
@@ -36,7 +40,6 @@
<uses-permission android:name="android.permission.SHUTDOWN" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission
android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"
android:maxSdkVersion="22" />
@@ -92,6 +95,8 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 获取模拟定位信息 -->
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
<!-- 健身运动权限,用于步数检测 -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<!-- 【必须】 移动推送 TPNS SDK 所需权限 -->
<!-- <uses-permission android:name="android.permission.INTERNET" /> -->
@@ -168,6 +173,7 @@
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.SplashActivity"
android:exported="true">
@@ -183,7 +189,6 @@
android:icon="@drawable/com_system_huyan"
android:label="护眼助手"
android:launchMode="singleInstance"
android:screenOrientation="userLandscape"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -233,7 +238,26 @@
android:launchMode="singleTask"
android:screenOrientation="behind"
android:theme="@style/DialogCloseOnTouchOutside" />
<activity
android:name=".activity.CameraDetectionActivity"
android:launchMode="singleTask" />
<activity
android:name=".activity.WarningDialogActivity"
android:theme="@style/TransparentActivity"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:taskAffinity="com.aoleyun.sn.warning" />
<service
android:name=".service.DetectionService"
android:enabled="true"
android:exported="true" />
<service
android:name=".service.ShakeDetectionService"
android:enabled="true"
android:exported="true" />
<service
android:name=".service.main.MainService"
@@ -422,6 +446,10 @@
<meta-data
android:name="com.alibaba.app.appsecret"
android:value="300dfca550f248598bc8b86c6cb9e76e" />
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="face" />
</application>
</manifest>

View File

@@ -0,0 +1,253 @@
package com.aoleyun.sn.activity;
import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.PointF;
import android.media.Image;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ExperimentalGetImage;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.aoleyun.sn.R;
import com.aoleyun.sn.detection.DistanceReminderManager;
import com.aoleyun.sn.detection.ShakeDetectionManager;
import com.aoleyun.sn.view.FaceOverlayView;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.common.InputImage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CameraDetectionActivity extends AppCompatActivity {
private static final String TAG = "CameraInputImage";
private static final int PERMISSION_REQUEST_CAMERA = 1001;
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
private ExecutorService cameraExecutor;
private PreviewView previewView;
private FaceOverlayView faceOverlay;
private android.widget.TextView tvStepInfo;
private int currentImageWidth;
private int currentImageHeight;
public static boolean isShowing = false;
private DistanceReminderManager reminderManager;
private ShakeDetectionManager shakeDetectionManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 如果你需要预览界面,可以使用 xml 里的 PreviewView这里仅演示后台获取数据流
setContentView(R.layout.activity_camera_detection);
previewView = findViewById(R.id.previewView);
faceOverlay = findViewById(R.id.faceOverlay);
tvStepInfo = findViewById(R.id.tvStepInfo);
// 初始化用于处理图像分析的单线程线程池,避免阻塞主线程
cameraExecutor = Executors.newSingleThreadExecutor();
// 检查并请求相机权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
}
reminderManager = new DistanceReminderManager(new DistanceReminderManager.OnDistanceAlertListener() {
@Override
public void onTooClose() {
// 触发提醒:弹出 Activity、播放提示音或让屏幕变模糊
WarningDialogActivity.start(CameraDetectionActivity.this, WarningDialogActivity.TYPE_DISTANCE);
}
@Override
public void onDistanceUpdate(double distance, PointF leftEye, PointF rightEye) {
// 传给 Overlay 进行比例换算
faceOverlay.updateData(distance, leftEye, rightEye, currentImageWidth, currentImageHeight);
}
});
// 初始化抖动(走路)检测
shakeDetectionManager = new ShakeDetectionManager(this, new ShakeDetectionManager.OnShakeListener() {
@Override
public void onShakeDetected() {
// 边走路边看平板提醒
WarningDialogActivity.start(CameraDetectionActivity.this, WarningDialogActivity.TYPE_SHAKE);
}
@Override
public void onStatusUpdate(String info) {
runOnUiThread(() -> {
if (tvStepInfo != null) {
tvStepInfo.setText(info);
}
});
}
});
}
/**
* 初始化并启动 CameraX
*/
private void startCamera() {
cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
bindCameraUseCases(cameraProvider);
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "初始化 CameraProvider 失败", e);
}
}, ContextCompat.getMainExecutor(this));
}
/**
* 配置并绑定相机用例
*/
private void bindCameraUseCases(@NonNull ProcessCameraProvider cameraProvider) {
// 1. 选择【前置摄像头】
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build();
// 2. 创建预览用例
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
// 3. 创建图像分析用例ImageAnalysis
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
// 设置背压策略:如果图像处理太慢,丢弃旧帧,保留最新的一帧(防积压爆内存)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// 建议设置输出的目标分辨率(例如 480x640 或 720x1280对于人脸/距离检测绰绰有余,且能提升运行速度
.setTargetResolution(new android.util.Size(640, 480))
.build();
// 3. 设置分析器,在这里把每一帧转换为 InputImage
imageAnalysis.setAnalyzer(cameraExecutor, new ImageAnalysis.Analyzer() {
private long lastAnalyzedTimestamp = 0L;
@OptIn(markerClass = ExperimentalGetImage.class)
@Override
public void analyze(@NonNull ImageProxy imageProxy) {
// 【性能调优】如果是防近视提醒不需要一秒分析30次。
// 这里限制为每 1000 毫秒1秒才执行一次算法
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp - lastAnalyzedTimestamp < 1000) {
imageProxy.close(); // 必须关闭,否则不会收到下一帧
return;
}
lastAnalyzedTimestamp = currentTimestamp;
// 从 imageProxy 中提取原生的 android.media.Image
Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
// 获取图像的旋转角度前置摄像头和设备方向有关ML Kit 需要这个角度来正确旋转识别)
int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
// 【核心代码】将原生 Image 转换为 ML Kit 的 InputImage
InputImage inputImage = InputImage.fromMediaImage(mediaImage, rotationDegrees);
// 此时已经成功拿到 InputImage可以传给你的算法或 ML Kit 识别器
// 【优化】传入 imageProxy 以便在异步任务完成后关闭它
processInputImage(inputImage, imageProxy);
} else {
// 如果获取不到 Image也需要关闭 imageProxy
imageProxy.close();
}
}
});
try {
// 在绑定前先解绑所有用例
cameraProvider.unbindAll();
// 将前置摄像头、预览和图像分析用例绑定到当前 Activity 的生命周期
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis);
} catch (Exception e) {
Log.e(TAG, "绑定相机用例失败", e);
}
}
/**
* 业务处理方法
*/
private void processInputImage(InputImage inputImage, ImageProxy imageProxy) {
// 在这里调用你的人脸检测或者瞳距计算算法
// Log.d(TAG, "成功获取到 InputImage尺寸: " + inputImage.getWidth() + "x" + inputImage.getHeight());
// 记录当前图像尺寸,供 UI 绘制缩放使用
// 重要InputImage 的 getWidth/Height 返回的是原始 buffer 尺寸,
// 而 ML Kit 的坐标是基于旋转后的。如果是 90/270 度旋转,宽高需要对调。
int rotation = imageProxy.getImageInfo().getRotationDegrees();
if (rotation == 90 || rotation == 270) {
currentImageWidth = inputImage.getHeight();
currentImageHeight = inputImage.getWidth();
} else {
currentImageWidth = inputImage.getWidth();
currentImageHeight = inputImage.getHeight();
}
// 比如传入上一问提到的人脸识别:
// 【关键修复】在 ML Kit 任务完成后再关闭 imageProxy避免 Internal error (ModelResource.zza)
reminderManager.checkDistance(inputImage)
.addOnCompleteListener(task -> imageProxy.close());
}
@Override
protected void onStart() {
super.onStart();
isShowing = true;
if (shakeDetectionManager != null) {
shakeDetectionManager.start();
}
}
@Override
protected void onStop() {
super.onStop();
isShowing = false;
if (shakeDetectionManager != null) {
shakeDetectionManager.stop();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关闭线程池,防止内存泄漏
if (cameraExecutor != null) {
cameraExecutor.shutdown();
}
}
// 处理权限回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CAMERA) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
WarningDialogActivity.start(this, WarningDialogActivity.TYPE_DISTANCE, "需要相机权限来检测距离");
}
}
}
}

View File

@@ -1,20 +1,28 @@
package com.aoleyun.sn.activity;
import android.content.Intent;
import android.provider.Settings;
import android.view.View;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.databinding.DataBindingUtil;
import com.aoleyun.sn.BuildConfig;
import com.aoleyun.sn.R;
import com.aoleyun.sn.base.BaseDataBindingActivity;
import com.aoleyun.sn.base.mvp.BaseMvpActivity;
import com.aoleyun.sn.comm.CommonConfig;
import com.aoleyun.sn.databinding.ActivityEyeProtectionBinding;
import com.aoleyun.sn.detection.SensorCheckUtil;
import com.aoleyun.sn.service.DetectionService;
import com.aoleyun.sn.service.ShakeDetectionService;
import com.aoleyun.sn.view.ToggleButton;
import com.hjq.toast.Toaster;
import com.tencent.mmkv.MMKV;
public class EyeProtectionActivity extends BaseDataBindingActivity {
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
private ActivityEyeProtectionBinding mBinding;
@@ -26,6 +34,7 @@ public class EyeProtectionActivity extends BaseDataBindingActivity {
@Override
protected void initDataBinding() {
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_eye_protection);
mBinding.setClick(new BtnClick());
}
@Override
@@ -65,6 +74,30 @@ public class EyeProtectionActivity extends BaseDataBindingActivity {
}
});
mBinding.toggleButton6.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
@Override
public void onToggle(boolean on) {
mMMKV.encode(CommonConfig.SHAKE_REMINDER, on ? 1 : 0);
if (on) {
startService(new Intent(EyeProtectionActivity.this, ShakeDetectionService.class));
} else {
stopService(new Intent(EyeProtectionActivity.this, ShakeDetectionService.class));
}
}
});
mBinding.toggleButton7.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
@Override
public void onToggle(boolean on) {
mMMKV.encode(CommonConfig.EYE_TO_SCREEN_DISTANCE_REMINDER, on ? 1 : 0);
if (on) {
startService(new Intent(EyeProtectionActivity.this, DetectionService.class));
} else {
stopService(new Intent(EyeProtectionActivity.this, DetectionService.class));
}
}
});
mBinding.clExit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -94,5 +127,29 @@ public class EyeProtectionActivity extends BaseDataBindingActivity {
} else {
mBinding.toggleButton2.setToggleOff();
}
int shakeReminder = mMMKV.decodeInt(CommonConfig.SHAKE_REMINDER, 0);
if (shakeReminder == 1) {
mBinding.toggleButton6.setToggleOn();
} else {
mBinding.toggleButton6.setToggleOff();
}
int reminder = mMMKV.decodeInt(CommonConfig.EYE_TO_SCREEN_DISTANCE_REMINDER, 0);
if (reminder == 1) {
mBinding.toggleButton7.setToggleOn();
} else {
mBinding.toggleButton7.setToggleOff();
}
}
public class BtnClick {
public void onDistanceClick(View view) {
if (BuildConfig.DEBUG) {
if (!SensorCheckUtil.hasProximitySensor(EyeProtectionActivity.this)) {
startActivity(new Intent(EyeProtectionActivity.this, CameraDetectionActivity.class));
}
}
}
}
}

View File

@@ -0,0 +1,85 @@
package com.aoleyun.sn.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.aoleyun.sn.R;
public class WarningDialogActivity extends AppCompatActivity {
public static final String EXTRA_TYPE = "extra_type";
public static final String EXTRA_MESSAGE = "extra_message";
public static final int TYPE_DISTANCE = 0;
public static final int TYPE_SHAKE = 1;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable finishRunnable = this::finish;
public static void start(Context context, int type) {
start(context, type, null);
}
public static void start(Context context, int type, String message) {
Intent intent = new Intent(context, WarningDialogActivity.class);
intent.putExtra(EXTRA_TYPE, type);
if (message != null) {
intent.putExtra(EXTRA_MESSAGE, message);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(intent);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_warning_dialog);
findViewById(R.id.iv_close).setOnClickListener(v -> finish());
findViewById(R.id.btn_confirm).setOnClickListener(v -> finish());
updateUI(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
updateUI(intent);
}
private void updateUI(Intent intent) {
int type = intent.getIntExtra(EXTRA_TYPE, TYPE_DISTANCE);
String customMessage = intent.getStringExtra(EXTRA_MESSAGE);
ImageView ivIcon = findViewById(R.id.iv_warning_icon);
TextView tvTitle = findViewById(R.id.tv_title);
TextView tvMessage = findViewById(R.id.tv_message);
if (type == TYPE_SHAKE) {
ivIcon.setImageResource(R.drawable.ic_warning_shake);
tvTitle.setText("检测到抖动");
tvMessage.setText(customMessage != null ? customMessage : "设备正在抖动,请确认是否为误触");
} else {
ivIcon.setImageResource(R.drawable.ic_warning);
tvTitle.setText("距离太近");
tvMessage.setText(customMessage != null ? customMessage : "请保持安全距离,保护您的视力");
}
handler.removeCallbacks(finishRunnable);
handler.postDelayed(finishRunnable, 3000);
}
@Override
public void finish() {
super.finish();
overridePendingTransition(0, android.R.anim.fade_out);
}
}

View File

@@ -198,17 +198,13 @@ public class MainViewModel extends BaseViewModel<ActivityMainBinding, ActivityEv
}
public void checkTestUpdate() {
if (!JgyUtils.getInstance().tagEmpty()) {
NetInterfaceManager.getInstance()
.checkTestUpdate(getLifecycle(), new NetInterfaceManager.onCompleteCallback() {
@Override
public void onComplete() {
NetInterfaceManager.getInstance()
.checkTestUpdate(getLifecycle(), new NetInterfaceManager.onCompleteCallback() {
@Override
public void onComplete() {
}
});
} else {
Log.e(TAG, "checkTestUpdate: tag is Empty");
}
}
});
}
public void getDefaultDesktop() {

View File

@@ -196,4 +196,9 @@ public class CommonConfig {
/*系统设置菜单选项*/
public final static String AOLE_SETTINGS_DISALLOW = "aole_settings_disallow";
public final static String EYE_TO_SCREEN_DISTANCE_REMINDER = "aole_eye_to_screen_distance_reminder";
public final static String SHAKE_REMINDER = "aole_shake_reminder";
}

View File

@@ -0,0 +1,97 @@
package com.aoleyun.sn.detection;
import android.graphics.PointF;
import android.util.Log;
import com.google.android.gms.tasks.Task;
import com.google.mlkit.vision.common.InputImage;
import com.google.mlkit.vision.face.Face;
import com.google.mlkit.vision.face.FaceDetection;
import com.google.mlkit.vision.face.FaceDetector;
import com.google.mlkit.vision.face.FaceDetectorOptions;
import com.google.mlkit.vision.face.FaceLandmark;
import java.util.List;
public class DistanceReminderManager {
private static final String TAG = "DistanceReminder";
// 【核心阈值】两眼间距离占图片长边的比例。
// 之前 640x480 分辨率下阈值为 220 像素220/640 ≈ 0.34
private static final double TOO_CLOSE_RATIO_THRESHOLD = 0.23;
private FaceDetector detector;
private OnDistanceAlertListener listener;
public interface OnDistanceAlertListener {
void onTooClose();
void onDistanceUpdate(double distance, PointF leftEye, PointF rightEye);
}
public DistanceReminderManager(OnDistanceAlertListener listener) {
this.listener = listener;
// 配置ML Kit开启关键点检测Landmark
FaceDetectorOptions options = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) // 追求速度
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) // 必须开启关键点
.build();
this.detector = FaceDetection.getClient(options);
}
/**
* 检测人脸距离的方法由相机帧回调定期调用例如每秒检测2-3次
*
* @return 返回 Task 供调用者管理生命周期(例如关闭 ImageProxy
*/
public Task<List<Face>> checkDistance(InputImage image) {
return detector.process(image)
.addOnSuccessListener(faces -> {
boolean faceFound = false;
for (Face face : faces) {
// 获取左眼和右眼的关键点
FaceLandmark leftEye = face.getLandmark(FaceLandmark.LEFT_EYE);
FaceLandmark rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE);
if (leftEye != null && rightEye != null) {
faceFound = true;
PointF leftEyePos = leftEye.getPosition();
PointF rightEyePos = rightEye.getPosition();
// 计算两眼之间的像素距离 (勾股定理)
double eyeDistance = Math.sqrt(
Math.pow(rightEyePos.x - leftEyePos.x, 2) +
Math.pow(rightEyePos.y - leftEyePos.y, 2)
);
Log.e(TAG, "checkDistance: 当前瞳距像素值: " + eyeDistance + ", 图片分辨率: " + image.getWidth() + "x" + image.getHeight());
if (listener != null) {
listener.onDistanceUpdate(eyeDistance, leftEyePos, rightEyePos);
}
// 计算归一化距离。
// 【优化】为了解决横屏判断距离比竖屏短的问题我们使用图像高度image.getHeight())作为基准。
// 在大多数传感器上,横屏时的高度 FOV 与竖屏时的宽度像素比例更接近,
// 使用高度作为分母可以有效补偿横竖屏切换时的像素密度差异,使判断距离趋于一致。
double normalizedDistance = eyeDistance / image.getHeight();
Log.e(TAG, "checkDistance: 归一化距离 (基于高度): " + normalizedDistance + ", 阈值: " + TOO_CLOSE_RATIO_THRESHOLD);
// 如果归一化距离大于阈值,说明离得太近了
if (normalizedDistance > TOO_CLOSE_RATIO_THRESHOLD) {
if (listener != null) {
listener.onTooClose();
}
}
}
}
if (!faceFound && listener != null) {
listener.onDistanceUpdate(0, null, null);
}
})
.addOnFailureListener(e -> Log.e(TAG, "人脸检测失败", e));
}
}

View File

@@ -0,0 +1,42 @@
package com.aoleyun.sn.detection;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.util.Log;
public class SensorCheckUtil {
/**
* 检测设备是否有距离感应器
*
* @param context 上下文
* @return true 有距离感应器false 没有
*/
public static boolean hasProximitySensor(Context context) {
// 1. 获取系统的传感器管理器
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
if (sensorManager == null) {
return false;
}
// 2. 获取默认的距离传感器
Sensor proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
// 3. 判断是否为空
if (proximitySensor != null) {
Log.e("SensorCheck", "找到距离感应器: " + proximitySensor.getName());
String name = proximitySensor.getName(); // 传感器名称
String vendor = proximitySensor.getVendor(); // 厂商
float maxRange = proximitySensor.getMaximumRange(); // 最大测量距离(通常厘米为单位,很多只有 0.0 或 5.0
Log.e("SensorDetails", "名称: " + name + ", 厂家: " + vendor + ", 最大范围: " + maxRange);
return true;
} else {
Log.e("SensorCheck", "此设备没有距离感应器硬件");
return false;
}
}
}

View File

@@ -0,0 +1,227 @@
package com.aoleyun.sn.detection;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.Log;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* 抖动检测管理类(用于实现边走路边看平板的护眼提醒)
* 优先使用系统步数检测器Step Detector若硬件不支持则自动回退到线性加速度方差算法。
*/
public class ShakeDetectionManager implements SensorEventListener {
private static final String TAG = "ShakeDetection";
private SensorManager sensorManager;
private Sensor stepSensor;
private Sensor accelSensor;
private OnShakeListener listener;
// --- 方案一:步数检测相关参数 ---
private final List<Long> stepTimestamps = new ArrayList<>();
// 判定为“正在走路”的阈值5秒内走了 4 步以上
private static final long STEP_TIME_WINDOW = 5000;
private static final int STEP_THRESHOLD = 4;
// --- 方案二:加速度方差相关参数 ---
private final Queue<Float> magnitudeWindow = new LinkedList<>();
// 采样窗口大小1.5 秒大约需要 25 个样本
private static final int VARIANCE_WINDOW_SIZE = 25;
// 提高方差阈值:正常的拿起放下抖动通常在 1.0~1.8 之间,我们将阈值提高到 2.0
private static final float VARIANCE_THRESHOLD = 2.0f;
// 增加评估频率控制:每 400ms 评估一次方差
private long lastVarianceEvalTime = 0;
private static final long VARIANCE_EVAL_INTERVAL = 400;
// 持续检测到抖动的评估次数:连续 4 次评估达标才判定400ms * 4 = 1.6秒的持续抖动)
private int consecutiveShakeCount = 0;
private static final int CONSECUTIVE_THRESHOLD = 4;
public interface OnShakeListener {
/**
* 当检测到持续抖动(疑似走路)时回调
*/
void onShakeDetected();
/**
* 实时步数或抖动强度更新
*
* @param info 描述信息(如 "步数: 3" 或 "抖动方差: 1.5"
*/
default void onStatusUpdate(String info) {
}
}
public ShakeDetectionManager(Context context, OnShakeListener listener) {
this.listener = listener;
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
if (sensorManager != null) {
// 1. 尝试获取步数检测传感器 (最精准、最省电)
stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
if (stepSensor != null) {
Log.d(TAG, "检测到硬件步数检测器: " + stepSensor.getName());
} else {
Log.d(TAG, "无硬件步数检测器: ");
}
// 2. 获取线性加速度计作为备选 (排除重力干扰)
accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
if (accelSensor != null) {
Log.d(TAG, "检测到线性加速度计: " + accelSensor.getName());
} else {
// 如果没有线性加速度计,降级使用普通加速度计
accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (accelSensor != null) {
Log.d(TAG, "未检测到线性加速度计,降级使用普通加速度计: " + accelSensor.getName());
}
}
}
if (stepSensor == null && accelSensor == null) {
Log.e(TAG, "错误:该设备不支持任何步数或加速度传感器,抖动检测功能将失效。");
}
}
public void start() {
if (sensorManager == null) return;
// 优先使用步数检测
if (stepSensor != null) {
Log.i(TAG, "启动检测:优先模式 [步数检测器]");
sensorManager.registerListener(this, stepSensor, SensorManager.SENSOR_DELAY_NORMAL);
} else if (accelSensor != null) {
Log.i(TAG, "启动检测:回退模式 [加速度方差算法]");
sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_UI);
}
}
public void stop() {
Log.d(TAG, "停止检测,重置状态。");
if (sensorManager != null) {
sensorManager.unregisterListener(this);
}
stepTimestamps.clear();
magnitudeWindow.clear();
consecutiveShakeCount = 0;
}
@Override
public void onSensorChanged(SensorEvent event) {
int sensorType = event.sensor.getType();
if (sensorType == Sensor.TYPE_STEP_DETECTOR) {
handleStepDetection();
} else if (sensorType == Sensor.TYPE_LINEAR_ACCELERATION || sensorType == Sensor.TYPE_ACCELEROMETER) {
handleAccelDetection(event);
}
}
/**
* 方案一:基于步数检测器的算法
*/
private void handleStepDetection() {
long currentTime = System.currentTimeMillis();
stepTimestamps.add(currentTime);
// 清除超出时间窗口的历史数据
long windowStartTime = currentTime - STEP_TIME_WINDOW;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stepTimestamps.removeIf(timestamp -> timestamp < windowStartTime);
} else {
// 兼容低版本
while (!stepTimestamps.isEmpty() && stepTimestamps.get(0) < windowStartTime) {
stepTimestamps.remove(0);
}
}
Log.v(TAG, "Step Detector: 检测到步伐,当前窗口步数=" + stepTimestamps.size());
if (listener != null) {
listener.onStatusUpdate("窗口步数: " + stepTimestamps.size());
}
// 如果在窗口内连续检测到足够步数
if (stepTimestamps.size() >= STEP_THRESHOLD) {
Log.w(TAG, "检测到用户正在走路 (步数模式)");
triggerCallback();
stepTimestamps.clear();
}
}
/**
* 方案二:基于加速度方差的算法
*/
private void handleAccelDetection(SensorEvent event) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
// 计算合加速度模长
float magnitude;
if (event.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) {
magnitude = (float) Math.sqrt(x * x + y * y + z * z);
} else {
// 普通加速度计减去重力近似值
magnitude = (float) Math.abs(Math.sqrt(x * x + y * y + z * z) - 9.8);
}
magnitudeWindow.add(magnitude);
if (magnitudeWindow.size() > VARIANCE_WINDOW_SIZE) {
magnitudeWindow.poll();
}
// 窗口填满后按固定频率计算方差
long currentTime = System.currentTimeMillis();
if (magnitudeWindow.size() == VARIANCE_WINDOW_SIZE && (currentTime - lastVarianceEvalTime > VARIANCE_EVAL_INTERVAL)) {
lastVarianceEvalTime = currentTime;
double variance = calculateVariance(magnitudeWindow);
if (variance > VARIANCE_THRESHOLD) {
consecutiveShakeCount++;
String info = String.format("抖动方差: %.2f, 连续计数: %d", variance, consecutiveShakeCount);
Log.d(TAG, info);
if (listener != null) {
listener.onStatusUpdate(info);
}
if (consecutiveShakeCount >= CONSECUTIVE_THRESHOLD) {
Log.w(TAG, "检测到用户正在走路 (加速度方差模式)");
triggerCallback();
consecutiveShakeCount = 0;
magnitudeWindow.clear();
}
} else {
// 没达到阈值时,清空计数,要求必须是连续的抖动
consecutiveShakeCount = 0;
}
}
}
private double calculateVariance(Queue<Float> queue) {
double sum = 0;
for (float val : queue) {
sum += val;
}
double mean = sum / queue.size();
double temp = 0;
for (float val : queue) {
temp += (val - mean) * (val - mean);
}
return temp / queue.size();
}
private void triggerCallback() {
if (listener != null) {
listener.onShakeDetected();
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}

View File

@@ -0,0 +1,316 @@
package com.aoleyun.sn.service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PointF;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.Image;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ExperimentalGetImage;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleService;
import com.aoleyun.sn.activity.CameraDetectionActivity;
import com.aoleyun.sn.activity.WarningDialogActivity;
import com.aoleyun.sn.detection.DistanceReminderManager;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.common.InputImage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 后台检测服务,用于持续监控人眼距离
* 移植自 CameraDetectionActivity
*/
public class DetectionService extends LifecycleService {
private static final String TAG = "DetectionService";
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
private ExecutorService cameraExecutor;
private DistanceReminderManager reminderManager;
private ImageAnalysis imageAnalysis;
private OrientationEventListener orientationEventListener;
private SensorManager sensorManager;
private Sensor proximitySensor;
private long lastAnalyzedTimestamp = 0L;
private final Handler watchdogHandler = new Handler(Looper.getMainLooper());
private static final long WATCHDOG_INTERVAL = 10000; // 10秒检测一次
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
// 初始化用于处理图像分析的单线程线程池
cameraExecutor = Executors.newSingleThreadExecutor();
// 初始化距离检测管理器
reminderManager = new DistanceReminderManager(new DistanceReminderManager.OnDistanceAlertListener() {
@Override
public void onTooClose() {
// 触发提醒
WarningDialogActivity.start(DetectionService.this, WarningDialogActivity.TYPE_DISTANCE);
}
@Override
public void onDistanceUpdate(double distance, PointF leftEye, PointF rightEye) {
// 后台服务通常不需要更新 UI Overlay如果需要可以将数据通过 LiveEventBus 或广播发送出去
}
});
// 启动相机(系统应用假设已获得权限)
startCamera();
// 监听屏幕旋转,优化检测准确性
initOrientationListener();
// 启动看门狗,定时检查摄像头状态
startWatchdog();
// 初始化传感器检测
initProximitySensor();
}
private void initProximitySensor() {
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
if (sensorManager != null) {
proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
if (proximitySensor != null) {
sensorManager.registerListener(proximityListener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
Log.d(TAG, "已注册距离传感器监听");
} else {
Log.d(TAG, "未找到距离传感器");
}
}
}
private final SensorEventListener proximityListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (CameraDetectionActivity.isShowing) {
return;
}
if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
float distance = event.values[0];
// 大多数接近传感器是二值的0或5或者返回实际厘米数
// 这里判断如果小于最大范围则认为过近
if (distance < proximitySensor.getMaximumRange()) {
Log.d(TAG, "距离传感器检测到距离过近: " + distance);
// WarningDialogActivity.start(DetectionService.this, WarningDialogActivity.TYPE_DISTANCE);
}
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
};
private void initOrientationListener() {
orientationEventListener = new OrientationEventListener(this) {
@Override
public void onOrientationChanged(int orientation) {
if (orientation == ORIENTATION_UNKNOWN) return;
// 将角度转换为 Surface.ROTATION
int rotation;
if (orientation >= 45 && orientation < 135) {
rotation = Surface.ROTATION_270;
} else if (orientation >= 135 && orientation < 225) {
rotation = Surface.ROTATION_180;
} else if (orientation >= 225 && orientation < 315) {
rotation = Surface.ROTATION_90;
} else {
rotation = Surface.ROTATION_0;
}
if (imageAnalysis != null) {
imageAnalysis.setTargetRotation(rotation);
}
}
};
orientationEventListener.enable();
}
private void startCamera() {
cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
bindCameraUseCases(cameraProvider);
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "初始化 CameraProvider 失败", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void bindCameraUseCases(@NonNull ProcessCameraProvider cameraProvider) {
if (CameraDetectionActivity.isShowing) {
Log.d(TAG, "CameraDetectionActivity 正在显示,跳过后台相机绑定");
return;
}
// 绑定时更新时间戳,给予缓冲期
lastAnalyzedTimestamp = System.currentTimeMillis();
// 1. 选择【前置摄像头】
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build();
// 2. 创建图像分析用例(无需 Preview 用例以节省资源)
ImageAnalysis.Builder builder = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(new android.util.Size(640, 480));
// 设置初始旋转方向
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
if (windowManager != null) {
builder.setTargetRotation(windowManager.getDefaultDisplay().getRotation());
}
imageAnalysis = builder.build();
// 3. 设置分析器
imageAnalysis.setAnalyzer(cameraExecutor, new ImageAnalysis.Analyzer() {
@OptIn(markerClass = ExperimentalGetImage.class)
@Override
public void analyze(@NonNull ImageProxy imageProxy) {
// 限制频率,每秒执行一次算法
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp - DetectionService.this.lastAnalyzedTimestamp < 1000) {
imageProxy.close();
return;
}
DetectionService.this.lastAnalyzedTimestamp = currentTimestamp;
Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
InputImage inputImage = InputImage.fromMediaImage(mediaImage, rotationDegrees);
// 调用检测逻辑
reminderManager.checkDistance(inputImage)
.addOnCompleteListener(task -> imageProxy.close());
} else {
imageProxy.close();
}
}
});
try {
// 在绑定前先解绑所有用例
cameraProvider.unbindAll();
// 将图像分析用例绑定到 LifecycleService 的生命周期
cameraProvider.bindToLifecycle(this, cameraSelector, imageAnalysis);
Log.d(TAG, "相机用例绑定成功");
} catch (Exception e) {
Log.e(TAG, "绑定相机用例失败", e);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
if (cameraExecutor != null) {
cameraExecutor.shutdown();
}
if (orientationEventListener != null) {
orientationEventListener.disable();
}
if (sensorManager != null) {
sensorManager.unregisterListener(proximityListener);
}
stopWatchdog();
}
private void startWatchdog() {
watchdogHandler.postDelayed(watchdogRunnable, WATCHDOG_INTERVAL);
}
private void stopWatchdog() {
watchdogHandler.removeCallbacks(watchdogRunnable);
}
private final Runnable watchdogRunnable = new Runnable() {
@Override
public void run() {
checkCameraStatus();
watchdogHandler.postDelayed(this, WATCHDOG_INTERVAL);
}
};
/**
* 定时检查摄像头状态,如果被抢占或未正常运行则尝试重新绑定
*/
private void checkCameraStatus() {
if (CameraDetectionActivity.isShowing) {
// 如果检测界面正在显示,后台服务主动释放摄像头,避免抢占
releaseCameraIfBound();
return;
}
if (cameraProviderFuture == null || !cameraProviderFuture.isDone()) {
return;
}
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
if (cameraProvider == null) return;
boolean isBound = false;
if (imageAnalysis != null) {
isBound = cameraProvider.isBound(imageAnalysis);
}
long now = System.currentTimeMillis();
// 如果未绑定,或者已绑定但超过 15 秒没有收到图像帧(说明摄像头可能被抢占或异常)
if (!isBound || (now - lastAnalyzedTimestamp > 15000)) {
Log.w(TAG, "检测到摄像头未运行或可能被抢占,尝试重新绑定... isBound=" + isBound);
bindCameraUseCases(cameraProvider);
}
} catch (Exception e) {
Log.e(TAG, "检查摄像头状态失败", e);
}
}
private void releaseCameraIfBound() {
if (cameraProviderFuture == null || !cameraProviderFuture.isDone()) {
return;
}
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
if (imageAnalysis != null && cameraProvider.isBound(imageAnalysis)) {
Log.d(TAG, "检测到 CameraDetectionActivity 正在运行,释放后台摄像头绑定");
cameraProvider.unbind(imageAnalysis);
}
} catch (Exception e) {
Log.e(TAG, "释放摄像头失败", e);
}
}
}

View File

@@ -0,0 +1,50 @@
package com.aoleyun.sn.service;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import com.aoleyun.sn.activity.WarningDialogActivity;
import com.aoleyun.sn.detection.ShakeDetectionManager;
public class ShakeDetectionService extends Service {
private static final String TAG = "ShakeDetectionService";
private ShakeDetectionManager shakeDetectionManager;
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "onCreate: ");
shakeDetectionManager = new ShakeDetectionManager(this, new ShakeDetectionManager.OnShakeListener() {
@Override
public void onShakeDetected() {
// 后台提醒
WarningDialogActivity.start(ShakeDetectionService.this, WarningDialogActivity.TYPE_SHAKE);
}
});
shakeDetectionManager.start();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy: ");
if (shakeDetectionManager != null) {
shakeDetectionManager.stop();
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@@ -49,10 +49,13 @@ import com.aoleyun.sn.bean.ScreenLockState;
import com.aoleyun.sn.comm.CommonConfig;
import com.aoleyun.sn.comm.JGYActions;
import com.aoleyun.sn.comm.PackageNames;
import com.aoleyun.sn.detection.SensorCheckUtil;
import com.aoleyun.sn.gson.GsonUtils;
import com.aoleyun.sn.hook.AoleyunActivityController;
import com.aoleyun.sn.network.NetInterfaceManager;
import com.aoleyun.sn.rlog.LogDBManager;
import com.aoleyun.sn.service.DetectionService;
import com.aoleyun.sn.service.ShakeDetectionService;
import com.aoleyun.sn.utils.ApkUtils;
import com.aoleyun.sn.utils.CacheUtils;
import com.aoleyun.sn.utils.ForegroundAppUtil;
@@ -481,6 +484,21 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
if (TextUtils.isEmpty(allowAppList)) {
Settings.System.putString(getContentResolver(), CommonConfig.SEEWO_APP_SOURCE_INSTALL_WHITELIST, String.join(",", JgyUtils.DEFAULT_APP_SOURCE_WHITE_LIST));
}
int reminder = mMMKV.decodeInt(CommonConfig.EYE_TO_SCREEN_DISTANCE_REMINDER, 0);
Log.e(TAG, "onCreate: reminder = " + reminder);
if (reminder == 1) {
if (!SensorCheckUtil.hasProximitySensor(this)) {
startService(new Intent(this, DetectionService.class));
}
}
int shakeReminder = mMMKV.decodeInt(CommonConfig.SHAKE_REMINDER, 0);
Log.e(TAG, "onCreate: shakeReminder = " + shakeReminder);
if (shakeReminder == 1) {
startService(new Intent(this, ShakeDetectionService.class));
}
}
private void initLiveData() {

View File

@@ -238,15 +238,13 @@ public class MainServiceModel extends ViewModel {
}
public void checkTestUpdate() {
if (!JgyUtils.getInstance().tagEmpty()) {
NetInterfaceManager.getInstance()
.checkTestUpdate(getLifecycle(), new NetInterfaceManager.onCompleteCallback() {
@Override
public void onComplete() {
NetInterfaceManager.getInstance()
.checkTestUpdate(getLifecycle(), new NetInterfaceManager.onCompleteCallback() {
@Override
public void onComplete() {
}
});
}
}
});
}
public void getDeveloper() {

View File

@@ -0,0 +1,95 @@
package com.aoleyun.sn.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.Locale;
public class FaceOverlayView extends View {
private Paint pointPaint;
private Paint textPaint;
private PointF leftEye;
private PointF rightEye;
private double distance;
private int imageWidth;
private int imageHeight;
public FaceOverlayView(Context context) {
super(context);
init();
}
public FaceOverlayView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
pointPaint = new Paint();
pointPaint.setColor(Color.RED);
pointPaint.setStyle(Paint.Style.FILL);
pointPaint.setStrokeWidth(10f);
textPaint = new Paint();
textPaint.setColor(Color.YELLOW);
textPaint.setTextSize(60f);
textPaint.setFakeBoldText(true);
textPaint.setShadowLayer(5, 0, 0, Color.BLACK);
}
public void updateData(double distance, PointF leftEye, PointF rightEye, int imageWidth, int imageHeight) {
this.distance = distance;
this.leftEye = leftEye;
this.rightEye = rightEye;
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (leftEye == null || rightEye == null || imageWidth <= 0 || imageHeight <= 0) {
canvas.drawText("未检测到人脸", 50, 100, textPaint);
return;
}
// 1. 获取 View 的尺寸
float viewWidth = getWidth();
float viewHeight = getHeight();
// 2. 计算缩放比例 (适配 PreviewView 的 FILL_CENTER 逻辑)
// FILL_CENTER取宽高缩放比例中较大的一个使图像填满 View 且不拉伸,多出的部分被裁剪
float scale = Math.max(viewWidth / imageWidth, viewHeight / imageHeight);
// 3. 计算偏移量(由居中裁剪产生)
float postScaleWidth = imageWidth * scale;
float postScaleHeight = imageHeight * scale;
float offsetX = (viewWidth - postScaleWidth) / 2f;
float offsetY = (viewHeight - postScaleHeight) / 2f;
// 4. 坐标映射并处理镜像
// CameraX 的 PreviewView 显示前置摄像头时默认是镜像的
// 映射公式: viewX = viewWidth - (imageX * scale + offsetX)
float lx = viewWidth - (leftEye.x * scale + offsetX);
float ly = leftEye.y * scale + offsetY;
float rx = viewWidth - (rightEye.x * scale + offsetX);
float ry = rightEye.y * scale + offsetY;
// 绘制眼睛点位
canvas.drawCircle(lx, ly, 15, pointPaint);
canvas.drawCircle(rx, ry, 15, pointPaint);
// 绘制连线,更直观
canvas.drawLine(lx, ly, rx, ry, pointPaint);
canvas.drawText(String.format(Locale.getDefault(), "瞳距: %.2f", distance), 50, 100, textPaint);
}
}

View File

@@ -28,11 +28,11 @@ public class ToggleButton extends View {
/**
* 开启颜色
*/
private int onColor = Color.parseColor("#00d56b");
private int onColor = Color.parseColor("#3a56e1");
/**
* 关闭颜色
*/
private int offBorderColor = Color.parseColor("#e7e4e4");
private int offBorderColor = Color.parseColor("#727272");
/**
* 灰色带颜色
*/
@@ -56,7 +56,7 @@ public class ToggleButton extends View {
/**
* 边框大小
*/
private int borderWidth = 2;
private int borderWidth = 3;
/**
* 垂直中心
*/

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorAccent" />
<corners android:radius="24dp" />
</shape>

View File

@@ -0,0 +1,27 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M447,59.6a132.7,132.7 0,0 1,178.3 46.6l0.1,0.2 380.5,633.2 0.3,0.6a132,132 0,0 1,-47.6 180.2,132.7 132.7,0 0,1 -65.7,18.2L131.1,938.7a132.7,132.7 0,0 1,-113.7 -66.7,132 132,0 0,1 0.4,-131.7l0.3,-0.6 380.5,-633.2 36.6,21.9 -36.5,-22.2a132.4,132.4 0,0 1,48.3 -46.6zM471.7,150.5l-0,0.1 -380.2,632.7a46.7,46.7 0,0 0,17.1 63.6c7,4.1 15.1,6.4 23.3,6.5L892.2,853.3a47.4,47.4 0,0 0,40.3 -23.7,46.7 46.7,0 0,0 0,-46.3L552.4,150.6l-0,-0.1A47.1,47.1 0,0 0,512 128a47.4,47.4 0,0 0,-40.3 22.5z"
android:fillColor="#fd7415" />
<path
android:pathData="M512,682.7a42.7,42.7 0,0 1,-42.7 -42.7V341.3a42.7,42.7 0,1 1,85.3 0v298.7a42.7,42.7 0,0 1,-42.7 42.7zM554.7,768a42.7,42.7 0,1 1,-85.3 0,42.7 42.7,0 0,1 85.3,0z"
android:fillColor="#fd7415" />
</vector>

View File

@@ -0,0 +1,24 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M677.6,95.9L345.5,95.9A103.6,103.6 0,0 0,244.3 195.8v629.9a102.4,102.4 0,0 0,101.8 102.4h331.9a103.1,103.1 0,0 0,104 -100L782,196a103.7,103.7 0,0 0,-104.6 -100zM720.3,824.6a41.3,41.3 0,0 1,-42 40.1L346.1,864.7a41.9,41.9 0,0 1,-42 -39.6L304.1,196.9a41.9,41.9 0,0 1,43.1 -37.9h331.9a43,43 0,0 1,42 40.1zM155.5,365.5L53.7,255.7a29.4,29.4 0,0 0,-42.6 0,29 29,0 0,0 0,42.3l87.9,96L16.6,482.4a41.5,41.5 0,0 0,0 54.9l81.9,88.4 -89.1,97.7a29,29 0,0 0,0 42.3,29.4 29.4,0 0,0 20.5,8.3 28.9,28.9 0,0 0,23.8 -11l101.2,-109.8a41,41 0,0 0,0 -54.9L73,511l82.4,-88.4a42,42 0,0 0,-0.3 -57zM927.2,627.5l81.9,-88.4a41.5,41.5 0,0 0,0 -54.9l-82.4,-90 89.6,-96.6a29.8,29.8 0,0 0,-1.7 -42.3,30.3 30.3,0 0,0 -42.6,1.7l-101.8,109.8a41.5,41.5 0,0 0,0 54.9l82.4,89 -81.9,87.9a41.5,41.5 0,0 0,0 54.9l101.2,109.8a28.9,28.9 0,0 0,22.1 9.9,30.4 30.4,0 0,0 27.5,-18.2 30,30 0,0 0,-5.4 -32.4z"
android:fillColor="#fd7415" />
</vector>

View File

@@ -6,6 +6,9 @@
<data>
<variable
name="click"
type="com.aoleyun.sn.activity.EyeProtectionActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@@ -60,44 +63,57 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_64"
android:layout_marginEnd="@dimen/dp_64"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginStart="@dimen/dp_64"
android:layout_marginEnd="@dimen/dp_64"
android:layout_marginTop="@dimen/dp_8"
android:background="@drawable/item_eye_background">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:maxLines="1"
android:text="滤蓝光模式"
android:textColor="@color/white"
android:textSize="@dimen/sp_11"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="开启滤蓝光护眼,让屏幕偏暖色,保护视力,减少视觉疲劳。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton1"
app:layout_constraintStart_toStartOf="@+id/textView1"
app:layout_constraintTop_toBottomOf="@+id/textView1" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="滤蓝光模式"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="开启滤蓝光护眼,让屏幕偏暖色,保护视力,减少视觉疲劳。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView1"
app:layout_constraintTop_toBottomOf="@+id/textView1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton1"
android:layout_width="@dimen/dp_34"
android:layout_height="@dimen/dp_18"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -108,40 +124,50 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginStart="@dimen/dp_64"
android:layout_marginTop="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_64"
android:background="@drawable/item_eye_background">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:maxLines="1"
android:text="阅读模式"
android:textColor="@color/white"
android:textSize="@dimen/sp_11"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="通过模拟纸张的色调,体验如墨水屏般的显示效果,保护视力。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton2"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="阅读模式"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="通过模拟纸张的色调,体验如墨水屏般的显示效果,保护视力。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton2"
android:layout_width="@dimen/dp_34"
android:layout_height="@dimen/dp_18"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -284,6 +310,115 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginTop="@dimen/dp_8"
android:background="@drawable/item_eye_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="抖动护眼提醒"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="边走路边看平板,通过弹窗提醒。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView6"
app:layout_constraintTop_toBottomOf="@+id/textView6" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton6"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginTop="@dimen/dp_8"
android:background="@drawable/item_eye_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
android:onClick="@{click::onDistanceClick}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="距离护眼提醒"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="长时间靠屏幕过近,通过弹窗提醒。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView7"
app:layout_constraintTop_toBottomOf="@+id/textView7" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton7"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,426 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
tools:context=".activity.EyeProtectionActivity">
<data>
<variable
name="click"
type="com.aoleyun.sn.activity.EyeProtectionActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1f2127">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_32"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_exit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_back"
android:layout_width="@dimen/dp_16"
android:layout_height="@dimen/dp_16"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="护眼功能"
android:textColor="@color/white"
android:textSize="@dimen/sp_13"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/iv_back"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/constraintLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_16"
android:layout_marginEnd="@dimen/dp_16"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginTop="@dimen/dp_8"
android:background="@drawable/item_eye_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="滤蓝光模式"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="开启滤蓝光护眼,让屏幕偏暖色,保护视力,减少视觉疲劳。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView1"
app:layout_constraintTop_toBottomOf="@+id/textView1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton1"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginTop="@dimen/dp_8"
android:background="@drawable/item_eye_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="阅读模式"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="通过模拟纸张的色调,体验如墨水屏般的显示效果,保护视力。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton2"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginStart="@dimen/dp_64"
android:layout_marginTop="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_64"
android:background="@drawable/item_eye_background"
android:visibility="gone">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:maxLines="1"
android:text="躺姿提醒(暂未开放)"
android:textColor="@color/white"
android:textSize="@dimen/sp_11"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="通仰躺、侧卧时使用平板,提醒坐正后使用"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton3"
app:layout_constraintStart_toStartOf="@+id/textView3"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton3"
android:layout_width="@dimen/dp_34"
android:layout_height="@dimen/dp_18"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginStart="@dimen/dp_64"
android:layout_marginTop="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_64"
android:background="@drawable/item_eye_background"
android:visibility="gone">
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:maxLines="1"
android:text="抖动提醒(暂未开放)"
android:textColor="@color/white"
android:textSize="@dimen/sp_11"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="走路、乘车等抖动环境下,提醒到平稳环境下使用"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton4"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toBottomOf="@+id/textView4" />
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton4"
android:layout_width="@dimen/dp_34"
android:layout_height="@dimen/dp_18"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginStart="@dimen/dp_64"
android:layout_marginTop="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_64"
android:background="@drawable/item_eye_background"
android:visibility="gone">
<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:maxLines="1"
android:text="亮度提醒(暂未开放)"
android:textColor="@color/white"
android:textSize="@dimen/sp_11"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="环境亮度过亮或过暗时,提醒到事宜的环境下使用"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton5"
app:layout_constraintStart_toStartOf="@+id/textView5"
app:layout_constraintTop_toBottomOf="@+id/textView5" />
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton5"
android:layout_width="@dimen/dp_34"
android:layout_height="@dimen/dp_18"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginTop="@dimen/dp_8"
android:background="@drawable/item_eye_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="抖动护眼提醒"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="边走路边看平板,通过弹窗提醒。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView6"
app:layout_constraintTop_toBottomOf="@+id/textView6" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton6"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_54"
android:layout_marginTop="@dimen/dp_8"
android:background="@drawable/item_eye_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
android:onClick="@{click::onDistanceClick}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggleButton7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="距离护眼提醒"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginEnd="@dimen/dp_16"
android:maxLines="2"
android:text="长时间靠屏幕过近,通过弹窗提醒。"
android:textColor="@color/white"
android:textSize="@dimen/sp_9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView7"
app:layout_constraintTop_toBottomOf="@+id/textView7" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.aoleyun.sn.view.ToggleButton
android:id="@+id/toggleButton7"
android:layout_width="@dimen/dp_26"
android:layout_height="@dimen/dp_14"
android:layout_marginEnd="@dimen/dp_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,35 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.aoleyun.sn.view.FaceOverlayView
android:id="@+id/faceOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/tvStepInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:layout_marginStart="16dp"
android:background="#80000000"
android:padding="8dp"
android:text="步数信息"
android:textColor="@android:color/white"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:layout_width="320dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:cardBackgroundColor="@android:color/white"
app:cardCornerRadius="24dp"
app:cardElevation="8dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_close"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:padding="4dp"
android:src="@drawable/icon_close" />
</RelativeLayout>
<ImageView
android:id="@+id/iv_warning_icon"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_warning" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="距离太近"
android:textColor="#333333"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:text="请保持安全距离,保护您的视力"
android:textColor="#666666"
android:textSize="14sp" />
<TextView
android:id="@+id/btn_confirm"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="32dp"
android:background="@drawable/bg_warning_btn_blue"
android:gravity="center"
android:text="我知道了"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

View File

@@ -95,4 +95,28 @@
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowCloseOnTouchOutside">true</item>
</style>
<!-- <style name="TransparentActivity" parent="Theme.AppCompat.Light.NoActionBar">-->
<!-- <item name="android:windowBackground">@android:color/transparent</item>-->
<!-- <item name="android:windowIsTranslucent">true</item>-->
<!-- <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>-->
<!-- <item name="android:windowNoTitle">true</item>-->
<!-- <item name="android:windowContentOverlay">@null</item>-->
<!-- <item name="android:backgroundDimEnabled">true</item>-->
<!-- <item name="android:backgroundDimAmount">0.3</item>-->
<!-- <item name="android:windowIsFloating">true</item>-->
<!-- <item name="android:windowTranslucentStatus">true</item>-->
<!-- <item name="android:statusBarColor">@android:color/transparent</item>-->
<!-- </style>-->
<style name="TransparentActivity" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.3</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>