feat: 新增护眼距离和行走抖动检测提醒功能
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -88,3 +88,4 @@ lint/tmp/
|
||||
/app/src/androidTest/java/com/jiaoguanyi/appstore/
|
||||
/adb
|
||||
/app/proguard-rules.pro
|
||||
/.codebuddy/
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
@@ -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, "需要相机权限来检测距离");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
316
app/src/main/java/com/aoleyun/sn/service/DetectionService.java
Normal file
316
app/src/main/java/com/aoleyun/sn/service/DetectionService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
95
app/src/main/java/com/aoleyun/sn/view/FaceOverlayView.java
Normal file
95
app/src/main/java/com/aoleyun/sn/view/FaceOverlayView.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
/**
|
||||
* 垂直中心
|
||||
*/
|
||||
|
||||
5
app/src/main/res/drawable/bg_warning_btn_blue.xml
Normal file
5
app/src/main/res/drawable/bg_warning_btn_blue.xml
Normal 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>
|
||||
27
app/src/main/res/drawable/ic_warning.xml
Normal file
27
app/src/main/res/drawable/ic_warning.xml
Normal 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>
|
||||
24
app/src/main/res/drawable/ic_warning_shake.xml
Normal file
24
app/src/main/res/drawable/ic_warning_shake.xml
Normal 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>
|
||||
@@ -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>
|
||||
426
app/src/main/res/layout-port/activity_eye_protection.xml
Normal file
426
app/src/main/res/layout-port/activity_eye_protection.xml
Normal 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>
|
||||
35
app/src/main/res/layout/activity_camera_detection.xml
Normal file
35
app/src/main/res/layout/activity_camera_detection.xml
Normal 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>
|
||||
79
app/src/main/res/layout/activity_warning_dialog.xml
Normal file
79
app/src/main/res/layout/activity_warning_dialog.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user