Commit 8ff902b4 authored by 张宏's avatar 张宏

2

parent eacff131
This diff is collapsed.
This diff is collapsed.
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/vibration-3.2.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider-2.1.5" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider-2.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2" />
...@@ -28,6 +27,9 @@ ...@@ -28,6 +27,9 @@
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/share_plus-12.0.2" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/share_plus-12.0.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/device_info_plus-12.4.0" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/device_info_plus-12.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/permission_handler-11.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/vibration-3.2.0" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES /> <SOURCES />
......
plugins { plugins {
id "com.android.application" id "com.android.application"
id "kotlin-android" id "kotlin-android"
...@@ -101,5 +99,5 @@ flutter { ...@@ -101,5 +99,5 @@ flutter {
} }
dependencies { dependencies {
// implementation 'com.github.jiangdongguo.AndroidUSBCamera:libausbc:3.3.3' implementation 'com.github.jiangdongguo.AndroidUSBCamera:libausbc:3.3.3'
} }
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- USB 摄像头权限 -->
<uses-feature android:name="android.hardware.usb.host" android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
<application <application
android:label="@string/app_name" android:label="@string/app_name"
android:name="${applicationName}" android:name="${applicationName}"
...@@ -23,6 +28,13 @@ ...@@ -23,6 +28,13 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<!-- USB 设备插拔事件监听 -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
......
package com.example.laki_icu_app package com.example.laki_icu_app
import com.example.laki_icu_app.camera.UsbCameraPlugin
import com.example.laki_icu_app.camera.UsbCameraViewFactory
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() { class MainActivity : FlutterActivity() {
companion object {
const val USB_CAMERA_CHANNEL = "usb_camera"
const val USB_CAMERA_VIEW_TYPE = "usb_camera_view"
}
private var usbCameraPlugin: UsbCameraPlugin? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 注册 PlatformViewFactory —— 用于在 Flutter 中渲染摄像头预览画面
flutterEngine
.platformViewsController
.registry
.registerViewFactory(USB_CAMERA_VIEW_TYPE, UsbCameraViewFactory())
// 注册 MethodChannel —— 用于 Flutter 与原生层通信(拍照、录像等控制指令)
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, USB_CAMERA_CHANNEL)
usbCameraPlugin = UsbCameraPlugin(this, channel)
}
override fun onDestroy() {
usbCameraPlugin?.dispose()
super.onDestroy()
}
} }
package com.example.laki_icu_app.camera
import android.content.Context
import android.hardware.usb.UsbDevice
import android.view.TextureView
import android.view.View
import com.jiangdg.ausbc.MultiCameraClient
import com.jiangdg.ausbc.callback.ICameraStateCallBack
import com.jiangdg.ausbc.callback.IPreviewDataCallBack
import com.jiangdg.ausbc.camera.bean.CameraRequest
import com.jiangdg.ausbc.camera.bean.PreviewSize
import io.flutter.plugin.common.MethodChannel
import java.io.File
/**
* USB 摄像头管理器(单例)
*
* 职责:
* - 封装 AndroidUSBCamera (libausbc) 的调用
* - 被 UsbCameraPlugin(MethodChannel 控制)和 UsbCameraView(预览画面)共享
*
* 注意:MultiCameraClient 支持多摄像头管理,当前实现先支持第一个 USB 摄像头
*/
object UsbCameraManager {
private var cameraClient: MultiCameraClient? = null
private var textureView: TextureView? = null
private var isOpened = false
private var currentDevice: UsbDevice? = null
/** 获取已连接的 USB 摄像头设备列表 */
fun getDeviceList(context: Context): List<Map<String, Any?>> {
val client = MultiCameraClient(context)
val list = client.getDeviceList()
client.release()
return list.map { device ->
mapOf<String, Any?>(
"deviceId" to device.deviceId,
"deviceName" to device.deviceName,
"vendorId" to device.vendorId,
"productId" to device.productId,
)
}
}
/** 打开指定 USB 摄像头 */
fun openCamera(
context: Context,
textureView: TextureView,
deviceId: Int? = null,
): Boolean {
if (isOpened) {
closeCamera()
}
return try {
val client = MultiCameraClient(context).apply {
// 设置状态回调
setCameraStateCallBack(object : ICameraStateCallBack {
override fun onCameraOpen() {
isOpened = true
}
override fun onCameraClose() {
isOpened = false
}
override fun onCameraError(message: String?) {
isOpened = false
}
override fun onCameraAttached(device: UsbDevice?) {
// USB 摄像头插入时触发
}
override fun onCameraDetached(device: UsbDevice?) {
// USB 摄像头拔出时触发
if (device == currentDevice) {
isOpened = false
}
}
})
}
// 打开摄像头
val config = CameraRequest.Builder()
.setPreviewSize(PreviewSize.PREVIEW_SIZE_720P)
.create()
client.initialize(textureView, config)
client.startPreview()
this.cameraClient = client
this.textureView = textureView
isOpened = true
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/** 拍照 */
fun takePhoto(savePath: String?): String? {
val client = cameraClient ?: return null
return try {
val path = savePath
?: "/storage/emulated/0/Pictures/usb_camera_${System.currentTimeMillis()}.jpg"
// 确保目录存在
val file = File(path)
file.parentFile?.mkdirs()
client.capturePicture(path)
path
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/** 开始录像 */
fun startRecord(savePath: String?): Boolean {
val client = cameraClient ?: return false
return try {
val path = savePath
?: "/storage/emulated/0/Movies/usb_camera_${System.currentTimeMillis()}.mp4"
val file = File(path)
file.parentFile?.mkdirs()
client.startRecording(path)
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/** 停止录像 */
fun stopRecord(): String? {
val client = cameraClient ?: return null
return try {
client.stopRecording()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/** 关闭摄像头 */
fun closeCamera() {
try {
cameraClient?.stopPreview()
cameraClient?.release()
} catch (_: Exception) {
// 忽略释放时的异常
}
cameraClient = null
textureView = null
isOpened = false
currentDevice = null
}
/** 查询摄像头是否已打开 */
fun isCameraOpened(): Boolean = isOpened
/** 设置预览分辨率 */
fun setPreviewSize(width: Int, height: Int): Boolean {
// libausbc 的 preview size 在初始化时设置,运行时无法动态切换
// 此处保留接口供后续扩展
return false
}
}
package com.example.laki_icu_app.camera
import android.app.Activity
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
/**
* USB 摄像头 MethodChannel 处理器
*
* 职责:
* - 接收 Flutter 端通过 MethodChannel 发来的控制指令
* - 调用 UsbCameraManager 执行对应操作
* - 通过 result 将结果返回给 Flutter 端
*
* MethodChannel 名称: "usb_camera"
*/
class UsbCameraPlugin(
private val activity: Activity,
private val channel: MethodChannel,
) : MethodChannel.MethodCallHandler {
init {
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getDeviceList" -> {
val list = UsbCameraManager.getDeviceList(activity)
result.success(list)
}
"openCamera" -> {
// 此方法由 PlatformView 在创建时通过 Flutter 端调用
// 实际的 openCamera 由 UsbCameraView 在 getView() 时完成
// 这里返回 true 表示准备就绪
result.success(true)
}
"closeCamera" -> {
UsbCameraManager.closeCamera()
result.success(true)
}
"takePhoto" -> {
val path = call.argument<String>("path")
val photoPath = UsbCameraManager.takePhoto(path)
if (photoPath != null) {
result.success(photoPath)
} else {
result.error("TAKE_PHOTO_ERROR", "拍照失败", null)
}
}
"startRecord" -> {
val path = call.argument<String>("path")
val success = UsbCameraManager.startRecord(path)
result.success(success)
}
"stopRecord" -> {
val videoPath = UsbCameraManager.stopRecord()
if (videoPath != null) {
result.success(videoPath)
} else {
result.error("STOP_RECORD_ERROR", "停止录像失败", null)
}
}
"isCameraOpened" -> {
result.success(UsbCameraManager.isCameraOpened())
}
"setPreviewSize" -> {
val width = call.argument<Int>("width") ?: 1280
val height = call.argument<Int>("height") ?: 720
val success = UsbCameraManager.setPreviewSize(width, height)
result.success(success)
}
else -> result.notImplemented()
}
}
fun dispose() {
UsbCameraManager.closeCamera()
}
}
package com.example.laki_icu_app.camera
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.SurfaceTexture
import android.view.TextureView
import android.view.View
import io.flutter.plugin.platform.PlatformView
/**
* USB 摄像头预览 PlatformView
*
* 职责:
* - 内嵌 AndroidUSBCamera 的 TextureView 渲染摄像头预览画面
* - 实现 PlatformView 接口以嵌入 Flutter Widget 树
*
* viewType 注册名称: "usb_camera_view"
*/
class UsbCameraView(
private val context: Context,
viewId: Int,
) : PlatformView {
/** 摄像头预览 TextureView */
private val textureView: TextureView = TextureView(context).apply {
// 当 TextureView 的 SurfaceTexture 就绪后,打开摄像头并开始预览
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(
surface: SurfaceTexture,
width: Int,
height: Int,
) {
// Surface 就绪后打开 USB 摄像头
UsbCameraManager.openCamera(context, this@apply)
}
override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture,
width: Int,
height: Int,
) {
// 尺寸变化时无需额外处理
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
// 当 Surface 销毁时不要关闭摄像头,因为可能是暂时性的
return true
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
// 每帧更新时回调
}
}
}
override fun getView(): View = textureView
override fun dispose() {
// PlatformView 销毁时释放摄像头资源
UsbCameraManager.closeCamera()
}
}
package com.example.laki_icu_app.camera
import android.content.Context
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
/**
* USB 摄像头 PlatformView 工厂
*
* 职责:
* - 告诉 Flutter 如何创建 UsbCameraView 实例
* - 在 MainActivity.configureFlutterEngine() 中注册
*
* 注册名称: "usb_camera_view"
*/
class UsbCameraViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
return UsbCameraView(context, viewId)
}
}
package com.example.smart_hotel_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- USB 摄像头设备过滤器 -->
<!-- 通用 USB 视频设备(UVC) -->
<usb-device
class="14"
subclass="1" />
<!-- 部分厂商特定的 USB 摄像头设备 -->
<usb-device
class="14"
subclass="2" />
<!-- 兼容更多 USB 视频类设备 -->
<usb-device class="239" subclass="2" />
</resources>
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
/// USB 摄像头服务层
///
/// 职责:
/// - 封装 Flutter 与 Android 原生层之间的 MethodChannel 通信
/// - 提供拍照、录像、设备查询等操作接口
/// - 处理摄像头权限申请
///
/// MethodChannel 名称: "usb_camera"
class UsbCameraService {
static const _channel = MethodChannel('usb_camera');
// ==================== 权限管理 ====================
/// 检查并请求摄像头权限
/// 返回 true 表示权限已授予
static Future<bool> requestCameraPermission() async {
// iOS 需要 CAMERA 权限,Android 需要 CAMERA 权限
if (Platform.isAndroid) {
final status = await Permission.camera.request();
return status.isGranted;
} else if (Platform.isIOS) {
final status = await Permission.camera.request();
return status.isGranted;
}
return false;
}
/// 检查摄像头权限是否已授予
static Future<bool> hasCameraPermission() async {
return await Permission.camera.isGranted;
}
// ==================== 设备管理 ====================
/// 获取已连接的 USB 摄像头设备列表
/// 返回 List<Map>,每个 Map 包含 deviceId、deviceName 等字段
static Future<List<Map<String, dynamic>>> getDeviceList() async {
try {
final result = await _channel.invokeMethod('getDeviceList');
if (result is List) {
return result.cast<Map<dynamic, dynamic>>().map((e) {
return Map<String, dynamic>.from(e);
}).toList();
}
return [];
} catch (e) {
debugPrint('UsbCameraService.getDeviceList error: $e');
return [];
}
}
// ==================== 摄像头控制 ====================
/// 打开摄像头(由 PlatformView 创建时自动触发)
/// 通常不需要手动调用
static Future<bool> openCamera({int? deviceId}) async {
try {
final result = await _channel.invokeMethod('openCamera', {
if (deviceId != null) 'deviceId': deviceId,
});
return result == true;
} catch (e) {
debugPrint('UsbCameraService.openCamera error: $e');
return false;
}
}
/// 关闭摄像头
static Future<bool> closeCamera() async {
try {
final result = await _channel.invokeMethod('closeCamera');
return result == true;
} catch (e) {
debugPrint('UsbCameraService.closeCamera error: $e');
return false;
}
}
/// 拍照
/// [path] 可选,指定照片保存路径;不传则使用默认路径
/// 返回照片文件的路径
static Future<String?> takePhoto({String? path}) async {
try {
final result = await _channel.invokeMethod('takePhoto', {
if (path != null) 'path': path,
});
return result as String?;
} catch (e) {
debugPrint('UsbCameraService.takePhoto error: $e');
return null;
}
}
/// 开始录像
/// [path] 可选,指定视频保存路径;不传则使用默认路径
static Future<bool> startRecord({String? path}) async {
try {
final result = await _channel.invokeMethod('startRecord', {
if (path != null) 'path': path,
});
return result == true;
} catch (e) {
debugPrint('UsbCameraService.startRecord error: $e');
return false;
}
}
/// 停止录像
/// 返回视频文件的路径
static Future<String?> stopRecord() async {
try {
final result = await _channel.invokeMethod('stopRecord');
return result as String?;
} catch (e) {
debugPrint('UsbCameraService.stopRecord error: $e');
return null;
}
}
/// 查询摄像头是否已打开
static Future<bool> isCameraOpened() async {
try {
final result = await _channel.invokeMethod('isCameraOpened');
return result == true;
} catch (e) {
debugPrint('UsbCameraService.isCameraOpened error: $e');
return false;
}
}
/// 设置预览分辨率
static Future<bool> setPreviewSize(int width, int height) async {
try {
final result = await _channel.invokeMethod('setPreviewSize', {
'width': width,
'height': height,
});
return result == true;
} catch (e) {
debugPrint('UsbCameraService.setPreviewSize error: $e');
return false;
}
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 摄像头控制按钮组件
///
/// 职责:
/// - 提供拍照、录像、刷新三个操作按钮
/// - 录像按钮支持录制中/停止录制两种状态切换
///
/// 涉及页面:首页监护舱页面 - 监控画面底部控制栏
class CameraControls extends StatelessWidget {
/// 是否正在录像
final bool isRecording;
/// 拍照回调
final VoidCallback? onTakePhoto;
/// 切换录像状态回调(开始/停止)
final VoidCallback? onToggleRecord;
/// 刷新回调
final VoidCallback? onRefresh;
const CameraControls({
super.key,
this.isRecording = false,
this.onTakePhoto,
this.onToggleRecord,
this.onRefresh,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildControlButton(
icon: Icons.refresh,
onPressed: onRefresh,
tooltip: '刷新画面',
),
SizedBox(width: 16.w),
_buildControlButton(
icon: Icons.camera_alt,
onPressed: onTakePhoto,
tooltip: '拍照',
),
SizedBox(width: 16.w),
_buildControlButton(
icon: isRecording ? Icons.stop : Icons.videocam,
onPressed: onToggleRecord,
tooltip: isRecording ? '停止录像' : '开始录像',
isActive: isRecording,
),
],
);
}
/// 构建单个控制按钮
Widget _buildControlButton({
required IconData icon,
VoidCallback? onPressed,
String? tooltip,
bool isActive = false,
}) {
return Tooltip(
message: tooltip ?? '',
child: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
onPressed?.call();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isActive
? const Color(0xCCFF4444)
: const Color(0x331E10B6),
borderRadius: BorderRadius.circular(30.r),
border: Border.all(
color: isActive ? Colors.redAccent : Colors.white24,
width: 1,
),
boxShadow: isActive
? [
BoxShadow(
color: Colors.redAccent.withValues(alpha: 0.4),
blurRadius: 12.r,
spreadRadius: 2,
),
]
: null,
),
child: Icon(
icon,
color: Colors.white,
size: 32.w,
),
),
),
);
}
}
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
class MonitoringScreen extends StatelessWidget { import 'camera_controls.dart';
/// 监控画面组件 —— 集成 USB 摄像头预览画面
///
/// 职责:
/// - 通过 AndroidView 嵌入 Android 原生 USBCameraTextureView 预览画面
/// - 底部控制栏:拍照、录像、刷新按钮
///
/// 涉及页面:首页监护舱页面
class MonitoringScreen extends StatefulWidget {
final VoidCallback? onRefresh; final VoidCallback? onRefresh;
final VoidCallback? onSnapshot; final VoidCallback? onSnapshot;
final VoidCallback? onRecord; final VoidCallback? onRecord;
...@@ -14,9 +24,16 @@ class MonitoringScreen extends StatelessWidget { ...@@ -14,9 +24,16 @@ class MonitoringScreen extends StatelessWidget {
}); });
@override @override
State<MonitoringScreen> createState() => _MonitoringScreenState();
}
class _MonitoringScreenState extends State<MonitoringScreen> {
bool _isRecording = false;
bool _cameraReady = false;
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 420.h,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
...@@ -28,27 +45,19 @@ class MonitoringScreen extends StatelessWidget { ...@@ -28,27 +45,19 @@ class MonitoringScreen extends StatelessWidget {
), ),
child: Stack( child: Stack(
children: [ children: [
Center( // 摄像头预览画面
child: Text( _buildCameraPreview(),
'监控画面', // 摄像头未就绪时的提示
style: TextStyle( if (!_cameraReady) _buildPlaceholder(),
fontSize: 40.sp, // 底部控制按钮
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
),
Positioned( Positioned(
bottom: 24.h, bottom: 24.h,
right: 24.w, right: 24.w,
child: Row( child: CameraControls(
children: [ isRecording: _isRecording,
_buildIconButton(Icons.refresh, onRefresh), onTakePhoto: _onTakePhoto,
SizedBox(width: 16.w), onToggleRecord: _onToggleRecord,
_buildIconButton(Icons.camera_alt, onSnapshot), onRefresh: widget.onRefresh,
SizedBox(width: 16.w),
_buildIconButton(Icons.videocam, onRecord),
],
), ),
), ),
], ],
...@@ -56,20 +65,165 @@ class MonitoringScreen extends StatelessWidget { ...@@ -56,20 +65,165 @@ class MonitoringScreen extends StatelessWidget {
); );
} }
Widget _buildIconButton(IconData icon, VoidCallback? onPressed) { /// 构建 USB 摄像头预览画面(通过 PlatformView)
return Container( Widget _buildCameraPreview() {
width: 60.w, // AndroidView 需要 Android API 20+,且仅在 Android 平台有效
height: 60.w, // 在其他平台或未连接 USB 摄像头时显示占位符
decoration: BoxDecoration( try {
color: const Color(0x331E10B6), return ClipRRect(
borderRadius: BorderRadius.circular(30.r), borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white24, width: 1), child: SizedBox.expand(
child: _buildAndroidView(),
),
);
} catch (_) {
return const SizedBox.shrink();
}
}
/// 构建 Android 原生 PlatformView
Widget _buildAndroidView() {
// ignore: undefined_platform_specific_widget
return AndroidView(
viewType: 'usb_camera_view',
onPlatformViewCreated: (_) {
setState(() {
_cameraReady = true;
});
},
creationParams: const <String, dynamic>{},
creationParamsCodec: const StandardMessageCodec(),
);
}
/// 未连接 USB 摄像头时的占位提示
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.videocam_off,
size: 60.w,
color: Colors.white38,
),
SizedBox(height: 16.h),
Text(
'USB 摄像头未连接',
style: TextStyle(
fontSize: 24.sp,
color: Colors.white54,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8.h),
Text(
'请插入 USB 摄像头设备',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white38,
),
),
],
), ),
child: IconButton( );
icon: Icon(icon, color: Colors.white, size: 32.w), }
onPressed: onPressed,
padding: EdgeInsets.zero, /// 拍照
Future<void> _onTakePhoto() async {
// 调用 Service 层拍照
final cameraService = await _getCameraService();
if (cameraService != null) {
final path = await cameraService.takePhoto();
if (path != null && mounted) {
_showSnackBar('拍照成功: $path');
}
}
widget.onSnapshot?.call();
}
/// 切换录像状态
Future<void> _onToggleRecord() async {
final cameraService = await _getCameraService();
if (cameraService == null) return;
if (_isRecording) {
final path = await cameraService.stopRecord();
if (path != null && mounted) {
_showSnackBar('录像已保存: $path');
}
setState(() => _isRecording = false);
} else {
final success = await cameraService.startRecord();
if (success) {
setState(() => _isRecording = true);
if (mounted) _showSnackBar('开始录像');
}
}
widget.onRecord?.call();
}
/// 获取摄像头服务实例
Future<dynamic> _getCameraService() async {
// 延迟导入,避免在非 Android 平台导入时出现问题
try {
// 动态调用 UsbCameraService
// ignore: depend_on_referenced_packages
return _UsbCameraServiceProxy();
} catch (_) {
return null;
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(24.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.r),
),
), ),
); );
} }
} }
/// USB 摄像头服务代理 —— 避免硬依赖导致非 Android 平台编译错误
class _UsbCameraServiceProxy {
Future<String?> takePhoto({String? path}) async {
// 通过 MethodChannel 直接调用(与 UsbCameraService 使用同一个 channel)
const channel = MethodChannel('usb_camera');
try {
final result = await channel.invokeMethod('takePhoto', {
if (path != null) 'path': path,
});
return result as String?;
} catch (_) {
return null;
}
}
Future<bool> startRecord({String? path}) async {
const channel = MethodChannel('usb_camera');
try {
final result = await channel.invokeMethod('startRecord', {
if (path != null) 'path': path,
});
return result == true;
} catch (_) {
return false;
}
}
Future<String?> stopRecord() async {
const channel = MethodChannel('usb_camera');
try {
final result = await channel.invokeMethod('stopRecord');
return result as String?;
} catch (_) {
return null;
}
}
}
...@@ -49,6 +49,7 @@ dependencies: ...@@ -49,6 +49,7 @@ dependencies:
infinite_scroll_pagination: ^5.1.1 infinite_scroll_pagination: ^5.1.1
logger: ^2.7.0 logger: ^2.7.0
fluttertoast: ^9.0.0 fluttertoast: ^9.0.0
permission_handler: ^11.3.1
# fl_chart: ^0.71.0 # fl_chart: ^0.71.0
# fl_chart: ^1.1.0 # fl_chart: ^1.1.0
......
...@@ -7,12 +7,15 @@ ...@@ -7,12 +7,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows flutter_secure_storage_windows
permission_handler_windows
share_plus share_plus
url_launcher_windows url_launcher_windows
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment