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

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查看最新代码

猜你喜欢

转载自blog.csdn.net/iotjin/article/details/126818977