草庐IT

Flutter - 图片/视频选择器(支持拍照及录制视频)

西半球 2023-06-23 原文

demo 地址: https://github.com/iotjin/jh_flutter_demo
代码不定时更新,请前往github查看最新代码

Flutter - 图片/视频选择器(支持拍照及录制视频)

之前基于一些图片选择库写了个图片选择器的组件Flutter - 实现多图选择,相机拍照功能,但是升级flutter2.0后作者不在更新了,一直没再管

升级flutter3.0后,基于wechat_assets_pickerwechat_camera_picker重新封装了个图片/视频选择器,这两个库是基于微信UI实现的,选择图片和拍照录像效果类似于微信,颜值比较高,简单封装即可使用

支持

  • 支持拍照/录制视频以及从图库选择资源
  • 设置资源选择类型(图片、视频、图片和视频)
  • 设置最大选择数量(默认9)
  • 设置最大视频录入时长(默认15秒)
  • 一行显示几个(默认3)
  • 图片视频全屏查看
  • 主题色跟随系统设置切换

效果图

库引用

# 手机权限 https://pub.flutter-io.cn/packages/permission_handler
permission_handler: ^9.2.0
# 图片视频选择器(微信UI) https://pub.flutter-io.cn/packages/wechat_assets_picker
wechat_assets_picker: ^8.0.2
# 拍照与录像(微信UI) https://pub.flutter-io.cn/packages/wechat_camera_picker
wechat_camera_picker: ^3.5.0+1

权限处理

  • Android - AndroidManifest.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">      
 </activity>
<!-- targetSdkVersion 31以上 (android12) 需要设置 android:exported="true"  -->

  • iOS - Info.plist
	<key>NSCameraUsageDescription</key>
	<string>是否允许"APP"使用您的相机,以便于进行拍照</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>是否允许"APP"访问您的相册,以便于保存图片</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>是否允许"APP"访问您的相册,以便于进行图片上传等操作</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>是否允许"APP"使用您的麦克风,以便于视频录制、语音识别、语音聊天</string>

因为我在底部弹框之前使用permission_handler做了权限判断,所以除了在info中加权限描述,还需要在Podfile文件加,否则iOS请求权限不弹框

# Permission权限配置
 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
     '$(inherited)',
     ## dart: PermissionGroup.camera
     'PERMISSION_CAMERA=1',
     ## dart: PermissionGroup.microphone
     'PERMISSION_MICROPHONE=1',
     ## dart: PermissionGroup.photos
     'PERMISSION_PHOTOS=1',
   ]

Example

import 'package:flutter/material.dart';
import '/jh_common/widgets/jh_asset_picker.dart';
import '/base_appbar.dart';

class PhotoSelectTest extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: backAppBar(context, 'PhotoSelect'),
      body: _body(),
    );
  }

  Widget _body() {
    return Container(
        color: Colors.yellow,
        padding: EdgeInsets.all(15),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('请选择图片(最多3张)'),
            SizedBox(height: 6),
            Container(
              padding: EdgeInsets.only(left: 5),
              child: JhAssetPicker(
                assetType: AssetType.image,
                maxAssets: 3,
                bgColor: Colors.red,
                callBack: (assetEntityList) async {
                  print('assetEntityList-------------');
                  print(assetEntityList);
                  if (assetEntityList.isNotEmpty) {
                    var asset = assetEntityList[0];
                    print(await asset.file);
                    print(await asset.originFile);
                  }
                  print('assetEntityList-------------');
                },
              ),
            ),
            SizedBox(height: 6),
            Text('请选择视频(最多1个)'),
            Container(
              padding: EdgeInsets.only(left: 5),
              child: JhAssetPicker(
                maxAssets: 1,
                assetType: AssetType.video,
                bgColor: Colors.red,
                callBack: (assetEntityList) {
                  print('assetEntityList-------------');
                  print(assetEntityList);
                  print('assetEntityList-------------');
                },
              ),
            ),
            Text('请选择图片或视频(一行展示4个)'),
            Container(
              padding: EdgeInsets.only(left: 5),
              child: JhAssetPicker(
                lineCount: 4,
                bgColor: Colors.red,
                callBack: (assetEntityList) {
                  print('assetEntityList-------------');
                  print(assetEntityList);
                  print('assetEntityList-------------');
                },
              ),
            ),
          ],
        ));
  }
}

源码

///  jh_asset_picker.dart
///
///  Created by iotjin on 2022/09/10.
///  description: 图片/视频选择器(支持拍照及录制视频) 封装wechat_assets_picker、wechat_camera_picker

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
import '/jh_common/widgets/jh_bottom_sheet.dart';
import '/jh_common/widgets/jh_progress_hud.dart';
import '/project/configs/colors.dart';
import '/project/provider/theme_provider.dart';

// 最大数量
const int _maxAssets = 9;
// 录制视频最长时长, 默认为 15 秒,可以使用 `null` 来设置无限制的视频录制
const Duration _maximumRecordingDuration = Duration(seconds: 15);
// 一行显示几个
const int _lineCount = 3;
// 每个GridView item间距(GridView四周与内部item间距在此统一设置)
const double _itemSpace = 5.0;
// 右上角删除按钮大小
const double _deleteBtnWH = 20.0;
// 默认添加图片
const String _addBtnIcon = 'assets/images/selectPhoto_add.png';
// 默认删除按钮图片
const String _deleteBtnIcon = 'assets/images/selectPhoto_close.png';
// 默认背景色
const Color _bgColor = Colors.transparent;

typedef _CallBack = void Function(List<AssetEntity> assetEntityList);

enum AssetType {
  image,
  video,
  imageAndVideo,
}

class JhAssetPicker extends StatefulWidget {
  const JhAssetPicker({
    Key? key,
    this.assetType = AssetType.image,
    this.maxAssets = _maxAssets,
    this.lineCount = _lineCount,
    this.itemSpace = _itemSpace,
    this.maximumRecordingDuration = _maximumRecordingDuration,
    this.bgColor = _bgColor,
    this.callBack,
  }) : super(key: key);

  final AssetType assetType; // 资源类型
  final int maxAssets; // 最大数量
  final int lineCount; // 一行显示几个
  final double itemSpace; // 每个GridView item间距(GridView四周与内部item间距在此统一设置)
  final Duration? maximumRecordingDuration; // 录制视频最长时长, 默认为 15 秒,可以使用 `null` 来设置无限制的视频录制
  final Color bgColor; // 背景色
  final _CallBack? callBack; // 选择回调

  
  _JhAssetPickerState createState() => _JhAssetPickerState();
}

class _JhAssetPickerState extends State<JhAssetPicker> {
  List<AssetEntity> _selectedAssets = [];
  Color _themeColor = KColors.kThemeColor;

  
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  
  Widget build(BuildContext context) {
    // TODO: 通过ThemeProvider进行主题管理
    final provider = Provider.of<ThemeProvider>(context);
    _themeColor = KColors.dynamicColor(context, provider.getThemeColor(), KColors.kThemeColor);

    var allCount = _selectedAssets.length + 1;
    return Container(
      color: widget.bgColor,
      child: GridView.builder(
        shrinkWrap: true,
        physics: NeverScrollableScrollPhysics(),
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          //可以直接指定每行(列)显示多少个Item
          crossAxisCount: widget.lineCount, //一行的Widget数量
          crossAxisSpacing: widget.itemSpace, //水平间距
          mainAxisSpacing: widget.itemSpace, //垂直间距
          childAspectRatio: 1.0, //子Widget宽高比例
        ),
        //GridView内边距
        padding: EdgeInsets.all(widget.itemSpace),
        itemCount: _selectedAssets.length == widget.maxAssets ? _selectedAssets.length : allCount,
        itemBuilder: (context, index) {
          if (_selectedAssets.length == widget.maxAssets) {
            return _itemWidget(index);
          }
          if (index == allCount - 1) {
            return _addBtnWidget();
          } else {
            return _itemWidget(index);
          }
        },
      ),
    );
  }

  // 添加按钮
  Widget _addBtnWidget() {
    return GestureDetector(
      child: Image(image: AssetImage(_addBtnIcon)),
      onTap: () => _showBottomSheet(),
    );
  }

  // 图片和删除按钮
  Widget _itemWidget(index) {
    return GestureDetector(
      child: Container(
        color: Colors.transparent,
        child: Stack(alignment: Alignment.topRight, children: <Widget>[
          ConstrainedBox(
            child: _loadAsset(_selectedAssets[index]),
            constraints: BoxConstraints.expand(),
          ),
          GestureDetector(
            child: Image(
              image: AssetImage(_deleteBtnIcon),
              width: _deleteBtnWH,
              height: _deleteBtnWH,
            ),
            onTap: () => _deleteAsset(index),
          )
        ]),
      ),
      onTap: () => _clickAsset(index),
    );
  }

  Widget _loadAsset(AssetEntity asset) {
    return Image(image: AssetEntityImageProvider(asset), fit: BoxFit.cover);
  }

  void _deleteAsset(index) {
    setState(() {
      _selectedAssets.removeAt(index);
      // 选择回调
      widget.callBack?.call(_selectedAssets);
    });
  }

  // 全屏查看
  void _clickAsset(index) {
    AssetPickerViewer.pushToViewer(
      context,
      currentIndex: index,
      previewAssets: _selectedAssets,
      themeData: AssetPicker.themeData(_themeColor),
    );
  }

  // 点击添加按钮
  void _showBottomSheet() {
    JhBottomSheet.showText(context, dataArr: ['拍照', '相册'], title: '请选择', clickCallback: (index, str) async {
      if (index == 1) {
        _openCamera();
      }
      if (index == 2) {
        _openAlbum();
      }
    });
  }

  // 相册选择
  Future<void> _openAlbum() async {
    // 相册权限
    final PermissionState ps = await PhotoManager.requestPermissionExtend();
    if (ps != PermissionState.authorized && ps != PermissionState.limited) {
      JhProgressHUD.showText('暂无相册权限,请前往设置开启权限');
      return;
    }

    RequestType requestType = RequestType.image;
    if (widget.assetType == AssetType.video) {
      requestType = RequestType.video;
    }
    if (widget.assetType == AssetType.imageAndVideo) {
      requestType = RequestType.common;
    }
    final List<AssetEntity>? result = await AssetPicker.pickAssets(
      context,
      pickerConfig: AssetPickerConfig(
        maxAssets: widget.maxAssets,
        requestType: requestType,
        selectedAssets: _selectedAssets,
        themeColor: _themeColor,
        // textDelegate: const EnglishAssetPickerTextDelegate(),
      ),
    );
    if (result != null) {
      setState(() {
        _selectedAssets = result;
      });
      // 相册选择回调
      widget.callBack?.call(result);
    }
  }

  // 拍照或录像
  Future<void> _openCamera() async {
    // 相机权限
    var isGrantedCamera = await Permission.camera.request().isGranted;
    if (!isGrantedCamera) {
      JhProgressHUD.showText('暂无相机权限,请前往设置开启权限');
      return;
    }

    if (widget.assetType != AssetType.image) {
      // 麦克风权限
      var isGrantedMicrophone = await Permission.microphone.request().isGranted;
      if (!isGrantedMicrophone) {
        JhProgressHUD.showText('暂无麦克风权限,请前往设置开启权限');
        return;
      }
    }

    // 相册权限
    final PermissionState ps = await PhotoManager.requestPermissionExtend();
    if (ps != PermissionState.authorized && ps != PermissionState.limited) {
      JhProgressHUD.showText('暂无相册权限,请前往设置开启权限');
      return;
    }

    final AssetEntity? result = await CameraPicker.pickFromCamera(
      context,
      pickerConfig: CameraPickerConfig(
        // 是否可以录像
        enableRecording: widget.assetType != AssetType.image,
        // 录制视频最长时长
        maximumRecordingDuration: widget.maximumRecordingDuration,
        // textDelegate: const EnglishCameraPickerTextDelegate(),
      ),
    );
    if (result != null) {
      setState(() {
        _selectedAssets.add(result);
        // 相机回调
        widget.callBack?.call(_selectedAssets);
      });
    }
  }
}

至此结束

demo 地址: https://github.com/iotjin/jh_flutter_demo
代码不定时更新,请前往github查看最新代码

有关Flutter - 图片/视频选择器(支持拍照及录制视频)的更多相关文章

  1. ruby-on-rails - Ruby on Rails - 为文本区域和图片生成列 - 2

    我是Rails的新手,所以请原谅简单的问题。我正在为一家公司创建一个网站。那家公司想在网站上展示它的客户。我想让客户自己管理这个。我正在为“客户”生成一个表格,我想要的三列是:公司名称、公司描述和Logo。对于名称,我使用的是name:string但不确定如何在脚本/生成脚手架终端命令中最好地创建描述列(因为我打算将其设置为文本区域)和图片。我怀疑描述(我想成为一个文本区域)应该仍然是描述:字符串,然后以实际形式进行调整。不确定如何处理图片字段。那么……说来话长:我在脚手架命令中输入什么来生成描述和图片列? 最佳答案 对于“文本”数

  2. ruby - Rails 3 的 RGB 颜色选择器 - 2

    状态:我正在构建一个应用程序,其中需要一个可供用户选择颜色的字段,该字段将包含RGB颜色代码字符串。我已经测试了一个看起来很漂亮但效果不佳的。它是“挑剔的颜色”,并托管在此存储库中:https://github.com/Astorsoft/picky-color.在这里我打开一个关于它的一些问题的问题。问题:请建议我在Rails3应用程序中使用一些颜色选择器。 最佳答案 也许页面上的列表jQueryUIDevelopment:ColorPicker为您提供开箱即用的产品。原因是jQuery现在包含在Rails3应用程序中,因此使用基

  3. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  4. 动漫制作技巧如何制作动漫视频 - 2

    动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、

  5. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  6. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  7. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  8. ruby-on-rails - Rails 单选按钮 - 模型中多列的一种选择 - 2

    我希望用户从一个模型的三个选项中选择一个。即我有一个模型视频,可以被评为正面/负面/未知目前我有三列bool值(pos/neg/unknown)。这是处理这种情况的最佳方式吗?为此,表单应该是什么样的?目前我有类似的东西但显然它允许多项选择,而我试图将它限制为只有一个..怎么办? 最佳答案 如果要使用字符串列,让我们说rating。然后在你的表单中:#...#...它只允许一个选择编辑完全相同但使用radio_button_tag: 关于ruby-on-rails-Rails单选按钮-模

  9. ruby-on-rails - CarrierWave - PDF - 只选择第一页 - 2

    我的Rails应用程序中安装了carrierwave。但是,当用户上传多页pdf时,我只希望应用程序获取文档中的第一页并将其转换为jpeg。这可能吗?用什么命令?这是我的uploader。#encoding:utf-8classImageUploader[200,300]##defscale(width,height)##dosomething#end#Createdifferentversionsofyouruploadedfiles:version:thumbdoprocess:resize_to_fill=>[150,210]process:convert=>:jpgdefful

  10. ruby-on-rails - ActiveAdmin 自定义选择过滤器下拉名称 - 2

    对于用户模型,我有一个过滤器来检查用户的预订状态,该状态由整数值(0、1或2)表示。UserActiveAdmin索引页上的过滤器是通过以下代码实现的:filter:booking_status,as::select然而,这会导致下拉选项为0、1或2。当管理员用户从下拉列表中选择它们时,我更愿意自己将它们命名为“未完成”、“待定”和“已确认”之类的名称。有没有办法在不改变booking_status在模型中的表示方式的情况下做到这一点? 最佳答案 假设booking_status是模型中的枚举字段,您可以使用:过滤器:booking

随机推荐