Flutter开发之图片选择器

使用FLutter开发了一个图片选择的组件,功能如下:

1、支持设置最大可选图片的个数;
2、根据选择的图片个数自适应容器组件的高度;
3、可设置容器的最大高度;
4、支持点击放大和删除功能;

具体效果如下

请添加图片描述

使用到的三方插件:

get: ^4.6.6 #路由管理
flutter_easyloading: ^3.0.5 #加载动画、弹框
image_picker: ^1.0.4 #图片选择器
photo_view: ^0.14.0 #点击图片处理–放大,缩放、滑动

代码如下:

1、先添加相册、相机权限

iOS

	<key>NSCameraUsageDescription</key>
	<string>更换个人头像需要使用相机功能</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>更换个人头像需要使用相册功能</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>更换个人头像需要使用相册功能</string>

Android

    <!-- 摄像头 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- 文件读取 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
    <!--    Android 13 READ_EXTERNAL_STORAGE权限需要使用READ_MEDIA_IMAGE、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO 来替代适配 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGE" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

2、创建一个ImagePickerMul的类

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:photo_view/photo_view.dart';

class ImagePickerMul extends StatefulWidget {
  final int maxCount; //最大选择图片数量
  final double maxHeight; //图片容器的最大高度
  final Function(List<XFile>) pickerImgCallBack; //选取图片成功之后拿到返回结果
  const ImagePickerMul({
    super.key,
    this.maxCount = 1000,
    this.maxHeight = 300.0,
    required this.pickerImgCallBack,
  });

  @override
  State<ImagePickerMul> createState() => _ImagePickerMulState();
}

class _ImagePickerMulState extends State<ImagePickerMul> {
  final List<XFile> _imageFileList = []; //存放图片的数组
  final ImagePicker _picker = ImagePicker();
  dynamic _pickerImageError;
  int _bigImageIndex = 0; //存放需要放大的图片下标

  // 获取当前展示图的数量
  int getImageCount() {
    widget.pickerImgCallBack(_imageFileList);
    if (_imageFileList.length < widget.maxCount) {
      return _imageFileList.length + 1;
    } else {
      return _imageFileList.length;
    }
  }

  void _onImageButtonPressed(
    ImageSource source, {
    double? maxHeight,
    double? maxWidth,
    int? imageQuality,
  }) async {
    try {
      final pickedFileList = await _picker.pickMultipleMedia(
        maxHeight: maxHeight,
        maxWidth: maxWidth,
        imageQuality: imageQuality,
      );
      setState(() {
        if (_imageFileList.length < widget.maxCount) {
          if ((_imageFileList.length + pickedFileList.length) <=
              widget.maxCount) {
            //加上新选的不能超过总数量
            for (var element in pickedFileList) {
              _imageFileList.add(element);
            }
          } else {
            EasyLoading.showToast(
              '最多只能选取${widget.maxCount}张图片,多余的图片将会自动删除!',
              duration: const Duration(milliseconds: 1500),
            );
            int avaliableCount = widget.maxCount - _imageFileList.length;
            for (int i = 0; i < avaliableCount; i++) {
              _imageFileList.add(pickedFileList[i]);
            }
          }
        }
      });
    } catch (e) {
      EasyLoading.showToast('$_pickerImageError');
      _pickerImageError = e;
    }
  }

  void _removeImage(int index) {
    setState(() {
      _imageFileList.removeAt(index);
    });
  }

  void _showBigImage(int index) {
    setState(() {
      _bigImageIndex = index;
    });
    //点击图片放大
    showDialog(
      context: context,
      builder: (context) {
        return Dialog(
          insetPadding: const EdgeInsets.only(left: 0.0),
          child: PhotoView(
            tightMode: true,
            imageProvider: FileImage(
              File(
                _imageFileList[_bigImageIndex].path,
              ),
            ),
          ),
        );
      },
    );
  }

  Widget? displayBigImage() {
    if (_imageFileList.length > _bigImageIndex) {
      return Image.file(
        File(
          _imageFileList[_bigImageIndex].path,
        ),
        fit: BoxFit.cover,
      );
    } else {
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    int columnCount = 0;
    if (_imageFileList.length == widget.maxCount) {
      columnCount = (widget.maxCount / 3).ceil();
    } else {
      columnCount = ((_imageFileList.length + 1) / 3).ceil();
    }
    return _imageFileList.isNotEmpty
        ? Container(
            height: columnCount * (Get.width / 3),
            constraints: BoxConstraints(
              maxHeight: widget.maxHeight,
            ),
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                childAspectRatio: 1.0,
              ),
              itemBuilder: (context, index) {
                if (_imageFileList.length < widget.maxCount) {
                  if (index < _imageFileList.length) {
                    return Padding(
                      padding: const EdgeInsets.all(10.0),
                      child: Stack(
                        alignment: Alignment.center,
                        children: [
                          Positioned(
                            top: 0.0,
                            left: 0.0,
                            bottom: 0.0,
                            right: 0.0,
                            child: GestureDetector(
                              child: Image.file(
                                File(
                                  _imageFileList[index].path,
                                ),
                                fit: BoxFit.cover,
                              ),
                              onTap: () => _showBigImage(index),
                            ),
                          ),
                          Positioned(
                            top: 0.0,
                            right: 0.0,
                            width: 20,
                            height: 20,
                            child: GestureDetector(
                              child: const Icon(
                                Icons.close,
                                color: Colors.white,
                              ),
                              onTap: () => _removeImage(index),
                            ),
                          ),
                        ],
                      ),
                    );
                  } else {
                    //显示添加符号
                    return Padding(
                      padding: const EdgeInsets.all(10.0),
                      child: IconButton(
                        onPressed: () => _onImageButtonPressed(
                          ImageSource.gallery,
                          imageQuality: 40, //图片压缩
                        ),
                        icon: const Icon(Icons.add_a_photo_outlined),
                      ),
                    );
                  }
                } else {
                  //选满了
                  return Container(
                    padding: const EdgeInsets.all(10.0),
                    child: Stack(
                      alignment: Alignment.center,
                      children: [
                        Positioned(
                          top: 0.0,
                          left: 0.0,
                          bottom: 0.0,
                          right: 0.0,
                          child: GestureDetector(
                            child: Image.file(
                              File(
                                _imageFileList[index].path,
                              ),
                              fit: BoxFit.cover,
                            ),
                            onTap: () => _showBigImage(index),
                          ),
                        ),
                        Positioned(
                          top: 0.0,
                          right: 0.0,
                          width: 20,
                          height: 20,
                          child: GestureDetector(
                            child: const SizedBox(
                              child: Icon(
                                Icons.close,
                                color: Colors.white,
                              ),
                            ),
                            onTap: () => _removeImage(index),
                          ),
                        ),
                      ],
                    ),
                  );
                }
              },
              itemCount: getImageCount(),
            ),
          )
        : SizedBox(
            width: 90,
            height: 90,
            child: IconButton(
              onPressed: () => _onImageButtonPressed(
                ImageSource.gallery,
                imageQuality: 40, //图片压缩
              ),
              icon: const Icon(Icons.add_a_photo_outlined),
            ),
          );
  }
}

3、使用

body: Container(
        color: Colors.yellow,
        child: ImagePickerMul(
          maxCount: 9,
          maxHeight: 300.0,
          pickerImgCallBack: (imageList) {},
        ),
      ),

本人将会持续更新在开发过程中遇到的各种小demo,如果喜欢的话,欢迎给个star,ღ( ´・ᴗ・` )比心

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/529339.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux系统安装内网穿透实现固定公网地址访问本地MinIO服务

文章目录 前言1. 创建Buckets和Access Keys2. Linux 安装Cpolar3. 创建连接MinIO服务公网地址4. 远程调用MinIO服务小结5. 固定连接TCP公网地址6. 固定地址连接测试 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&am…

【计算机考研】408算法大题怎么练?

先说结论&#xff1a;基础阶段学好各个数据结构与&#xff0c;重点是数组、链表、树、图。然后强化阶段突破算法提 在基础阶段&#xff0c;并不需要过于专门地练习算法。相反&#xff0c;基础阶段的重点应该放在对各种数据结构原理的深入理解上。在我个人的经验中&#xff0c;…

计算机考研择校|408还是自命题,哪个上岸难度大?

我一般是建议选择408&#xff0c;但是现在考408的同学太多了 所以408的竞争压力会比较大&#xff0c;加上复习难度大&#xff0c;复习过程中&#xff0c;心态很容易崩掉。 其实到底选自命题还是408&#xff0c;我觉得还是要看自己的目标。如果目标院校是自命题&#xff0c;那…

C语言的显式类型转换和隐式类型转换详细讲解

目录 一、类型转换 1、显式类型转换 2、隐式类型转换 二、算术转换 三、总结 每个编译器都会对表达式做两件事情&#xff0c;一是判断表达式中操作符的优先级和结合性&#xff0c;二是判断表达式中的操作数类型是否一致&#xff0c;如果不一致则需要进行类型转换。第一点在…

吴恩达机器学习实践实验室:决策树(Decision Trees)

在本练习中&#xff0c;您将从头开始实施决策树&#xff0c;并将其应用于蘑菇可食用还是有毒的分类任务。 文章目录 1-包2-问题陈述3-数据集3.1一个热编码数据集 4-决策树4.1计算熵4.2分割数据集4.3计算信息增益4.4获得最佳分割 5-构建树 1-包 首先&#xff0c;让我们运行下面…

【问题解决】ubuntu安装新版vscode报code-insiders相关错误

问题 目前 vscode官网 最新的包为 insiders_1.89.0-1712297812_amd64.deb &#xff0c;双击或者使用sudo dpkg -i code-insiders_1.89.0-1712297812_amd64.deb安装后报错&#xff0c;执行其他命令也报错。 安装环境&#xff1a;ubuntu18.04 dpkg: 处理软件包 code-insiders (…

代码随想录算法训练营第四十九天 | 121. 买卖股票的最佳时机、22. 买卖股票的最佳时机 II

代码随想录算法训练营第四十九天 | 121. 买卖股票的最佳时机、22. 买卖股票的最佳时机 II 121. 买卖股票的最佳时机题目解法 122. 买卖股票的最佳时机 II题目解法 感悟 121. 买卖股票的最佳时机 题目 解法 题解链接 没看题解想出来的&#xff1a;贪心 class Solution { pub…

归档模式下,物理删除数据文件的完全的恢复

归档模式下&#xff0c;物理删除数据文件的完全的恢复 1、实验环境 环境归档模式 SQL> archive log list Database log mode Archive Mode Automatic archival Enabled Archive destination /arch/archivelog Oldest online log seq…

用户态网络缓冲区的设计

一、网络缓冲区 在内核中也是有网络缓冲区的&#xff0c;比如使用 read 读取数据&#xff08;read 是一种系统调用&#xff0c;第一个参数为 fd&#xff09;&#xff0c;当陷入到内核态的时候&#xff0c;会通过 fd 指定 socket&#xff0c;socket 会找到对应的接收缓冲区。在…

抓住风口,快速上手RAG应用开发!

免责声明~ 任何文章不要过度深思&#xff01; 万事万物都经不起审视&#xff0c;因为世上没有同样的成长环境&#xff0c;也没有同样的认知水平&#xff0c;更「没有适用于所有人的解决方案」&#xff1b; 不要急着评判文章列出的观点&#xff0c;只需代入其中&#xff0c;适度…

37-代码测试(下):Go语言其他测试类型及IAM测试介绍

。 Go中的两类测试&#xff1a;单元测试和性能测试。 我就来介绍下Go 语言中的其他测试类型&#xff1a;示例测试、TestMain函数、Mock测试、Fake测试等&#xff0c; 示例测试 示例测试以Example开头&#xff0c;没有输入和返回参数&#xff0c;通常保存在example_test.go…

Go语言实现Redis分布式锁2

项目地址: https://github.com/liwook/Redislock 1.支持阻塞式等待获取锁 之前的是只尝试获取一次锁&#xff0c;要是获取失败就不再尝试了。现在修改为支持阻塞式等待获取锁。 添加LockOptions结构体 添加option.go文件。 在LockOptions中 isBlock表示是否是阻塞模式blo…

美团一面:说说synchronized的实现原理?问麻了。。。。

引言 在现代软件开发领域&#xff0c;多线程并发编程已经成为提高系统性能、提升用户体验的重要手段。然而&#xff0c;多线程环境下的数据同步与资源共享问题也随之而来&#xff0c;处理不当可能导致数据不一致、死锁等各种并发问题。为此&#xff0c;Java语言提供了一种内置…

Pots(DFS BFS)

//新生训练 #include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std; typedef pair<int, int> PII; const int N 205; int n, m; int l; int A, B, C; int dis[N][N];struct node {int px, py, op…

谱重排变换和同步压缩变换的区别是什么?

谱重排方法能够得到非常高的时频分辨率&#xff0c;但是同样也存在一个问题&#xff0c;不能重构原始信号&#xff0c;2011 年 Daubechies 提出了一种基于相位的高分辨率时频分析方法—同步压缩小波变换&#xff0c;该方法也是一种谱重排的方法&#xff0c;能使非平稳非线性信号…

Mybatis报错:Unsupported conversion from LONG to java.sql.Timestamp

Mybatis在封装结果集的时候&#xff0c;如果方法返回的是对象&#xff0c;则会去调用这个对象的无参构造方法。 如果实体类标注了Builder注解&#xff0c;则此注解会把默认的构造方法全部改成私有的&#xff0c;则Mybatis在通过无参构造方法反射创建对象时&#xff0c;就会找不…

Redis中的集群(二)

节点 集群数据结构 redisClient结构和clusterLink结构的相同和不同之处 redisClient结构和clusterLink结构都有自己的套接字描述符和输入、输出缓冲区&#xff0c;这两个结构的区别在于&#xff0c;redisClient结构中的套接字和缓冲区是用于连接客户端的&#xff0c;而clust…

已解决:windows 下无法加载文件 xxx.ps1,因为在此系统上禁止运行脚本

目录 1&#xff0c;问题描述2&#xff0c;问题解决 1&#xff0c;问题描述 当通过 npm 全局安装依赖后&#xff08;比如 ts 对应的 tsc 命令&#xff0c;还有 pnpm&#xff09;&#xff0c;想直接使用安装的命令&#xff0c;就会报错&#xff1a; 2&#xff0c;问题解决 以管…

2024年AI带来的革命性变革与创新

大家好&#xff01;相信大家对于AI&#xff08;人工智能&#xff09;的发展已经有了一定的了解&#xff0c;但你是否意识到&#xff0c;到了2024年&#xff0c;AI已经变得如此强大和普及&#xff0c;带来了我们从未想象过的便利和创新呢&#xff1f;让我们一起来看看AI在这个时…

Python学习笔记11 - 列表

1. 列表的创建与删除 2. 列表的查询操作 3. 列表的增、删、改操作 4. 列表元素的排序 5. 列表生成式