首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Flutter WeChat 风格相机与相册选择器【介绍、示例、避坑】

  • 25-04-18 03:41
  • 3745
  • 11700
juejin.cn

Flutter WeChat 风格相机与相册选择器

在 Flutter 开发中,常常需要实现图片/视频选择功能,比如从相册选择图片、拍照上传等。wechat_assets_picker 和 wechat_camera_picker 是优秀的第三方库,提供了类似微信的 UI 和交互体验,本文将详细讲解如何在 Flutter 项目中集成这两个库,并处理相关权限问题。

1. 简介

wechat_assets_picker 和 wechat_camera_picker 是基于微信 UI 实现的 Flutter 库,可用于图片选择和拍照/录像,界面美观且易于集成。

支持平台: Android、iOS

功能特点:

  • 拍照 / 录制视频 / 从图库选择资源
  • 资源类型设置(图片、视频或两者兼有)
  • 最大选择数量设置(默认 9)
  • 最大视频录制时长设置(默认 15 秒)
  • 网格布局每行显示数量(默认 3)
  • 图片 / 视频全屏查看
  • 主题色随系统切换

2. 依赖库引入📦

yaml
代码解读
复制代码
# 权限申请 permission_handler: ^11.3.1 # 相册选择器 wechat_assets_picker: ^9.2.1 # 相机拍摄器 wechat_camera_picker: ^4.3.2

然后运行:

dart
代码解读
复制代码
flutter pub get

3. 权限处理

Android

在 AndroidManifest.xml 中添加权限:

xml
代码解读
复制代码
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.FLASHLIGHT" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> <activity android:exported="true" android:requestLegacyExternalStorage="true" />

注意: 如果 targetSdkVersion >= 31(Android 12),需要设置 android:exported="true"。

iOS

在 Info.plist 添加权限描述:

xml
代码解读
复制代码
<key>NSCameraUsageDescriptionkey> <string>是否允许 "APP" 使用您的相机,以便拍照string> <key>NSPhotoLibraryAddUsageDescriptionkey> <string>是否允许 "APP" 访问您的相册,以便保存图片string> <key>NSPhotoLibraryUsageDescriptionkey> <string>是否允许 "APP" 访问您的相册,以便上传图片string> <key>NSMicrophoneUsageDescriptionkey> <string>是否允许 "APP" 使用您的麦克风,以便录制视频string>

特别注意:此外,在 Podfile 添加权限宏,否则 iOS 可能不会弹出权限请求框:(这是一个非常坑的地方)

ruby
代码解读
复制代码
target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'PERMISSION_CAMERA=1', 'PERMISSION_MICROPHONE=1','PERMISSION_PHOTOS=1'] end

4. 使用示例

注:包含引导弹窗和权限不足时跳转到权限设置页

4.1 打开相机

dart
代码解读
复制代码
void onTapCamera() async { try { if (!(await checkCameraPermissions())) return; if (!(await checkPhotosPermissions())) return; final AssetEntity? entity = await CameraPicker.pickFromCamera( Get.context!, pickerConfig: const CameraPickerConfig( enableAudio: false, enableRecording: false, resolutionPreset: ResolutionPreset.max, ), ); if (entity == null) return; final file = await entity.file; if (file == null) throw Exception("无法获取文件"); final String filePath = file.path; LogUtil.info("捕获的图片路径: $filePath"); } catch (e) { LogUtil.error("拍照出错: $e"); } }

4.2 打开相册

dart
代码解读
复制代码
void onTapAlbum() async { try { if (!(await checkPhotosPermissions())) return; final List? assets = await AssetPicker.pickAssets( Get.context!, pickerConfig: AssetPickerConfig( maxAssets: 9, requestType: RequestType.image, ), ); if (assets != null) { for (var asset in assets) { final String? filePath = await asset.file.then((file) => file?.path); if (filePath != null) { LogUtil.info("选择的图片路径: $filePath"); } } } } catch (e) { LogUtil.error("打开相册出错: $e"); } }

5. 权限检查与处理

5.1 检查相机权限

dart
代码解读
复制代码
Future<bool> checkCameraPermissions() async { if (!(await Permission.camera.request().isGranted)) { await showPermissionDeniedDialog("相机权限不足"); return false; } return true; }

5.2 检查相册权限

dart
代码解读
复制代码
Future<bool> checkPhotosPermissions() async { if (!(await PhotoManager.requestPermissionExtend()).isAuthorized) { await showPermissionDeniedDialog("相册权限不足"); return false; } return true; }

5.3 权限不足提示弹窗

dart
代码解读
复制代码
Future<void> showPermissionDeniedDialog(String message) async { await Get.dialog( Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("权限不足", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), SizedBox(height: 16), Text(message, textAlign: TextAlign.center), SizedBox(height: 24), ElevatedButton( onPressed: () { Get.back(); PhotoManager.openSetting(); }, child: Text("前往设置"), ), ], ), ), ), barrierDismissible: false, ); }

6.macOS需要使用文件选择器来选择文件。

文件选择器

dart
代码解读
复制代码
file_picker: ^8.1.0
dart
代码解读
复制代码
// macos使用文件选择器获取图片 if (Platform.isMacOS) { final result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, ); if (result != null && result.files.isNotEmpty) { for (var file in result.files) { final String? filePath = file.path; if (filePath != null) { LogUtil.info("Image path: $filePath"); var message = generatePictureMessage(curChatID.value, filePath); var msgData = displayMessage( indexController.selectedGroup.value.groupType, message); sendUploadPicture(filePath, msgData); } } } return; }

6. 结语

通过 wechat_assets_picker 和 wechat_camera_picker,可以轻松实现微信风格的相机与相册选择功能,同时支持权限检查和异常处理,保证应用的稳定性与用户体验。

完整代码示例如下:

dart
代码解读
复制代码
// 打开相机 void onTapCamera() async { try { // 检查相机权限 if (!(await checkCameraPermissions())) { return; // 相机权限不足,直接返回 } // 检查相册权限 if (!(await checkPhotosPermissions())) { return; // 相册权限不足,直接返回 } // 权限通过,执行拍照逻辑 final AssetEntity? entity = await CameraPicker.pickFromCamera( Get.context!, locale: Get.locale, pickerConfig: const CameraPickerConfig( enableAudio: false, enableRecording: false, onlyEnableRecording: false, resolutionPreset: ResolutionPreset.max, //最大分辨率 ), ); if (entity == null) { LogUtil.info("No photo captured."); return; } final file = await entity.file; if (file == null) { throw Exception("Failed to fetch file from entity."); } final String filePath = file.path; LogUtil.info("Captured image path: $filePath"); var message = generatePictureMessage(curChatID.value, filePath); var msgData = displayMessage( indexController.selectedGroup.value.groupType, message); sendUploadPicture(filePath, msgData); } catch (e) { LogUtil.error("Error capturing image: $e"); } } // 打开相册 void onTapAlbum() async { try { // 检查相册权限 if (!(await checkPhotosPermissions())) { return; } final AssetPickerConfig config = AssetPickerConfig( textDelegate: assetPickerTextDelegateFromLocale(Get.locale), maxAssets: 9, requestType: RequestType.image, ); final List? assets = await AssetPicker.pickAssets( Get.context!, pickerConfig: config, ); toolsVisible.toggle(); if (assets != null) { for (var asset in assets) { final String? filePath = await asset.file.then((file) => file?.path); if (filePath != null) { LogUtil.info("Image path: $filePath"); // 生成消息并展示本地图片(展示并保存) var message = generatePictureMessage(curChatID.value, filePath); var msgData = displayMessage( indexController.selectedGroup.value.groupType, message); sendUploadPicture(filePath, msgData); } } } } catch (e) { LogUtil.error("Error onTapAlbum image: $e"); } } Future<bool> checkCameraPermissions() async { bool? isRead = DataSp.getPermissionGuideStatus(); if (isRead == null || !isRead) { await showPermissionsGuideDialog(); } // 请求相机权限 var status = await Permission.camera.request(); if (!status.isGranted) { bool? hasAlreadyDenied = DataSp.getCameraPermissionDeniedStatus(); // 如果是首次拒绝权限,不弹出权限不足提示 if (hasAlreadyDenied == null || !hasAlreadyDenied) { // 标记第一次拒绝权限 DataSp.putCameraPermissionDeniedStatus(true); return false; } // 后续拒绝时提示权限不足 await showPermissionDeniedDialog( descData: tr('permissionCameraInsufficient')); return false; } // 延时操作避免缓存问题 await Future.delayed(const Duration(milliseconds: 500)); return true; } Future<bool> checkPhotosPermissions() async { bool? isRead = DataSp.getPermissionGuideStatus(); if (isRead == null || !isRead) { await showPermissionsGuideDialog(); } // 请求相册权限 PermissionState permissionState = await PhotoManager.requestPermissionExtend(); if (permissionState != PermissionState.authorized && permissionState != PermissionState.limited) { bool? hasAlreadyDenied = DataSp.getPhotosPermissionDeniedStatus(); // 如果是第一次拒绝权限,不弹出权限不足提示 if (hasAlreadyDenied == null || !hasAlreadyDenied) { // 标记第一次拒绝权限 DataSp.putPhotosPermissionDeniedStatus(true); return false; } // 后续检查时提示权限不足 await showPermissionDeniedDialog( descData: tr('permissionPhotoInsufficient')); return false; } // 延时操作避免缓存问题 await Future.delayed(const Duration(milliseconds: 500)); return true; } Future<void> showPermissionDeniedDialog({required String descData}) async { await Get.dialog( WillPopScope( onWillPop: () async => false, // 禁止返回按钮关闭弹窗 child: Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15.0), ), child: Container( padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: Colors.white, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( tr('permissionInsufficient'), style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Text( descData, textAlign: TextAlign.center, ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: ElevatedButton( onPressed: Get.back, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[100], foregroundColor: Colors.grey[600], padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), // 按钮圆角 ), ), child: Text( tr('cancel'), style: TextStyle( fontSize: 16, fontFamily: 'Alibaba PuHuiTi 3.0', fontWeight: FontWeight.w700, ), ), ), ), const SizedBox(width: 15), Expanded( child: ElevatedButton( onPressed: () { Get.back(); // 关闭弹窗 PhotoManager.openSetting(); // 跳转到系统设置页面 }, style: ElevatedButton.styleFrom( backgroundColor: Styles.c_YellowColor, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: Text( tr('goToSettings'), style: TextStyle( color: Colors.white, fontSize: 16, fontFamily: 'Alibaba PuHuiTi 3.0', fontWeight: FontWeight.w700, ), ), ), ), ], ), ], ), ), ), ), barrierDismissible: false, // 点击背景不允许关闭弹窗 ); } // 显示按钮引导弹窗 Future<void> showPermissionsGuideDialog() async { await Get.dialog( WillPopScope( onWillPop: () async => false, // 禁止返回按钮关闭弹窗 child: Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: Container( padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: Colors.white, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 引导标题 Text( tr('permissionsGuildTitle'), style: TextStyle( fontWeight: FontWeight.bold, fontSize: 20, color: Colors.black87, ), textAlign: TextAlign.center, ), SizedBox(height: 20), // 第一部分:相册图标和文字 Row( children: [ Icon( Icons.photo_library, size: 40, color: Colors.blueAccent, ), SizedBox(width: 10), Expanded( child: Text( tr('photosDesc'), style: TextStyle(fontSize: 16, color: Colors.black54), ), ), ], ), SizedBox(height: 20), // 第二部分:相机图标和文字 Row( children: [ Icon( Icons.camera_alt, size: 40, color: Colors.blueAccent, ), SizedBox(width: 10), Expanded( child: Text( tr('cameraDesc'), style: TextStyle(fontSize: 16, color: Colors.black54), ), ), ], ), SizedBox(height: 30), // 继续按钮 ElevatedButton( onPressed: () { Navigator.of(Get.context!).pop(); DataSp.putPermissionGuideStatus(true); }, style: ElevatedButton.styleFrom( backgroundColor: Styles.c_YellowColor, foregroundColor: Colors.white, minimumSize: Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), ), child: Text( tr('continue'), style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ], ), ), ), ), ); }
注:本文转载自juejin.cn的飞川001的文章"https://juejin.cn/post/7474424122249642034"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top