Flutter图片放大(双击缩放、双指滑动缩放、拖拽)

2023-11-13

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:cached_network_image/cached_network_image.dart';

class ZoomImage extends StatefulWidget {
  const ZoomImage({Key key, this.url}) : super(key: key);
  final url;
  @override
  State<StatefulWidget> createState() {
    return _ZoomImage();
  }
}

class _ZoomImage extends State<ZoomImage> with SingleTickerProviderStateMixin{
  AnimationController _controller;
  Animation<Offset> _animation;
  Offset _offset = Offset.zero;
  double _scale = 1.0;
  Offset _normalizedOffset;
  double _previousScale;
  double _kMinFlingVelocity = 600.0;
  bool _isEnlarge = false;
  bool _isHideTitleBar = false;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _controller.addListener(() {
      setState(() {
        _offset = _animation.value;
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  Offset _clampOffset(Offset offset) {
    final Size size = context.size;
    // widget的屏幕宽度
    final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale);
    // 限制他的最小尺寸
    return Offset(
        offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));
  }

  _handleOnScaleStart(ScaleStartDetails details) {
    setState(() {
      _isHideTitleBar = true;
      _previousScale = _scale;
      _normalizedOffset = (details.focalPoint - _offset) / _scale;
      // 计算图片放大后的位置
      _controller.stop();
    });
  }

  _handleOnScaleUpdate(ScaleUpdateDetails details) {
    setState(() {
      _scale = (_previousScale * details.scale).clamp(1.0, 3.0);
      // 限制放大倍数 1~3倍
      _offset = _clampOffset(details.focalPoint - _normalizedOffset * _scale);
      // 更新当前位置
    });
  }

  _handleOnScaleEnd(ScaleEndDetails details) {
    _setSystemUi();
    final double magnitude = details.velocity.pixelsPerSecond.distanceSquared;
    if (magnitude < _kMinFlingVelocity) return;
    final Offset direction = details.velocity.pixelsPerSecond / magnitude;
    // 计算当前的方向
    final double distance = (Offset.zero & context.size).shortestSide;
    // 计算放大倍速,并相应的放大宽和高,比如原来是600*480的图片,放大后倍数为1.25倍时,宽和高是同时变化的
    _animation = _controller.drive(Tween<Offset>(
        begin: _offset, end: _clampOffset(_offset + direction * distance)));
    _controller
      ..value = 0.0
      ..fling(velocity: magnitude / 1000.0);
  }

  _onDoubleTap() {
    _isHideTitleBar = true;
    _setSystemUi();
    Size size = context.size;
    _isEnlarge  = !_isEnlarge;
    setState(() {
      if(!_isEnlarge){
        _scale = 2.0;
        _offset = Offset(-(size.width/2), -(size.height/2));
      }else{
        _scale = 1.0;
        _offset = Offset.zero;
      }

    });
  }
  _onTap(){
    setState(() {
      _isHideTitleBar = !_isHideTitleBar;
    });
    _setSystemUi();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        _bodyView(),
        _titleBar()
      ],
    );
  }

  _bodyView(){
    return GestureDetector(
      onScaleStart: _handleOnScaleStart,
      onScaleUpdate: _handleOnScaleUpdate,
      onScaleEnd: _handleOnScaleEnd,
      onDoubleTap: _onDoubleTap,
      onTap: _onTap,
      child: Container(
        color: _isHideTitleBar?Colors.black:Colors.white,
        child: SizedBox.expand(
          child: ClipRect(
            child: Transform(
              transform: Matrix4.identity()..translate(_offset.dx, _offset.dy)
                ..scale(_scale),
              child: CachedNetworkImage(
                imageUrl: widget.url,
                fit:BoxFit.contain,
                // placeholder: (context, url) => CircularProgressIndicator(),
                errorWidget: (context, url, error) => Icon(Icons.warning_amber_rounded,size: 15,),
              ),
            ),
            // child: Image.network(widget.url,fit: BoxFit.cover,),
          ),
        ),
      ),
    );
  }

  _titleBar(){
    return Offstage(
      child: Container(
        alignment: Alignment.centerLeft,
        padding: EdgeInsets.only(top: MediaQueryData.fromWindow(window).padding.top,
        left: ScreenUtil().setWidth(24)),
        color: Colors.white,
        height: MediaQuery.of(context).size.height * 0.1,
        width: MediaQuery.of(context).size.width,
        child: GestureDetector(
          child: Icon(Icons.arrow_back),
          onTap: (){
            Navigator.pop(context);
          },
        ),
      ),
      offstage: _isHideTitleBar,
    );
  }

  _setSystemUi(){
    if(_isHideTitleBar){
      SystemChrome.setEnabledSystemUIOverlays([]);
    }else{
      SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
    }
  }

}

参考:flutter_drag_scale

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Flutter图片放大(双击缩放、双指滑动缩放、拖拽) 的相关文章

随机推荐

  • 编译linux内核(一)

    关于linux启动流程 1 第一阶段 BIOS 1 1 硬件自检 1 2 启动顺序 2 第二阶段 主引导记录 2 1 主引导记录的结构 2 2 分区表 3 第三阶段 硬盘启动 3 1 情况A 卷引导记录 3 2 情况B 扩展分区和逻辑分区
  • 图像数据格式uint8与double以及图像类型转换

    1 图像数据格式 double 64位 matlab中数值一般采用double型存储和运算 uint8 8位无符号整数 为了节省存储空间 matlab为图像提供的特殊数据类型 imread把灰度图像存入一个8位矩阵 当为RGB图像时 就存入
  • 慕课第6周在线第2题 计算阶乘的和v2.0

    计算阶乘的和v2 0 4分 题目内容 假设有这样一个三位数m 其百位 十位和个位数字分别是a b c 如果m a b c 则这个三位数就称为三位阶乘和数 约定0 1 请编程计算并输出所有的三位阶乘和数 函数原型 long Fact int
  • 【参加CUDA线上训练营】共享内存实例1:矩阵转置实现及其优化①

    参加CUDA线上训练营 共享内存实例1 矩阵转置实现及其优化 1 完整代码 2 原理介绍 2 1 将各block 线程对应元素放入共享内存tile 2 2 实现转置 2 3 在此基础上修改 参考文献 本文参考Nvidia官方blog An
  • 唐伯虎诗集

    妒花 昨夜海棠初着雨 数点轻盈娇欲语 佳人晓起出兰房 折来对镜化红妆 问郎花好奴颜好 郎道不如花窈窕 佳人闻语发娇嗔 不信死花胜活人 将花揉碎掷郎前 请郎今日伴花眠 观鳌山 禁卫森严夜寂寥 灯山忽见翠召荛 六鳌并驾神仙府 双鹊连成帝子桥 星
  • 【H5】 图片拖入浏览器readAsDataURL与createObjectURL处理图片:

    H5 外部图片拖入浏览器 效果图如下 const file new FileReader 读到文件对象的信息 file readAsDataURL oFile 读取到文件所以的url 需要转入对象 方法一 实现代码如下
  • MongoDB下载安装与测试连接(windows)

    一 MongoDB下载 MongoDB为Windows提供了两种安装方式 分别是msi和zip方式 msi方式对4 0以上版本进行了优化 而3 x版本则需在安装后手动进行配置 zip方式解压即安装 但需要配置 故4 0以上版本推荐使用msi
  • 使用citavi快速搜索论文、下载论文

    第一步 点击在线搜索 第二步 一般需要添加下需要的数据库 这里我们添加常用的arxiv 第三步 输入需要搜索的论文名字 也可以通过作者来搜索 以及选择年份等等 第四步 添加搜索到的论文 最后 打开这个pdf 点击保存一个复制 这里也可以通过
  • 逆向某视频直播软件,破解收费观看

    https juejin im post 5cbd7bc06fb9a0324e4a376c
  • 深度操作系统Deepin V20正式版

    前言 深度科技在9月11日发布了 深度操作系统Deepin V20正式版 Deepin Desktop Community 名如其意 深度桌面系统社区版 对个人用户完全免费 没用上UOS企业版及个人版的完全可以安装Deepin V20尝尝鲜
  • vue-qr 二维码 批量 导出

    参考Vue批量生成二维码并打包下载 首先我们需要安装三个插件 jszip zip打包 file saver 文件保存 vue qr 二维码 完整代码如下 src components QRcode index vue
  • flex布局(项目-即子级)

    基本概念 采用Flex布局的元素 称为Flex容器 flex container 简称 容器 它的所有子元素自动成为容器成员 称为Flex项目 flex item 简称 项目 项目的属性 以下6个属性设置在项目上 order flex gr
  • DataOutputStream 类 和DatainputStream类 的主要方法简单介绍,及代码演示。

    DataOutputStream数据输出流 将java基本数据类型写入数据输出流中 并可以通过数据输入流DataInputStream将数据读入 DataOutputStream类 构造函数 DataOutputStream OutputS
  • Qt在QTableWidget、View等表格中添加右击菜单

    先来看效果图 鼠标点在哪里菜单显示在哪里 实现代码 在构造函数中设置右击菜单 并关联右击的槽函数 ui tableWidget gt setContextMenuPolicy Qt CustomContextMenu 设置右击菜单 conn
  • cov() missing 1 required positional argument: ‘other‘报错

    原代码 prets np dot weights log returns mean 252 计算年化风险 diag是获取对角线的数据 pvols np diag np sqrt np dot weights np dot log retur
  • 定制android ROM禁止SIM卡状态改变系统弹出提示框

    按上一篇文章 搭建可修改android 系统的环境 http blog csdn net karts article details 64124098 SIM卡状态改变系统弹出提示框 是在 Keyguard 这个apk里面进行的 1 cop
  • go语言基础-----19-----Context使用原则、接口、派生上下文(select的多路复用可以参考这里理解更好)

    1 Go语言Context介绍 为什么需要 Context 每一个处理都应该有个超时限制 需要在调用中传递这个超时 比如开始处理请求的时候我们说是 3 秒钟超时 那么在函数调用中间 这个超时还剩多少时间了 需要在什么地方存储这个信息 这样请
  • 安全意识培训:如何提高员工网络安全意识?

    随着网络技术的不断发展和应用 网络安全已经成为企业必须关注和重视的问题 尤其是在今天 企业数字化转型的大背景下 网络安全问题日益凸显 对于企业而言 员工是企业安全的第一道防线 提高员工的网络安全意识已经成为企业安全管理的关键所在 本文将从以
  • 【实践篇】推荐算法PaaS化探索与实践

    作者 京东零售 崔宁 1 背景说明 目前 推荐算法部支持了主站 企业业务 全渠道等20 业务线的900 推荐场景 通过梳理大促运营 各垂直业务线推荐场景的共性需求 对现有推荐算法能力进行沉淀和积累 并通过算法PaaS化打造通用化的推荐能力
  • Flutter图片放大(双击缩放、双指滑动缩放、拖拽)

    import dart ui import package flutter material dart import package flutter services dart import package flutter screenut