Flutter-自定义图片3D画廊

news2024/11/16 1:23:42
效果

111.gif

需求
  • 3D画廊效果
设计内容
  • Stack
  • GestureDetector
  • Transform
  • Positioned
  • 数学三角函数
代码实现

具体代码大概300行

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_xy/widgets/xy_app_bar.dart';

import '../../r.dart';

class ImageSwitchPage extends StatefulWidget {
  const ImageSwitchPage({Key? key}) : super(key: key);

  @override
  State<ImageSwitchPage> createState() => _ImageSwitchPageState();
}

class _ImageSwitchPageState extends State<ImageSwitchPage> {
  var imgList = [
    R.img1_jpg,
    R.img2_jpg,
    R.img3_jpg,
  ];

  var deviationRatio = 0.8;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: XYAppBar(
        title: "3D画廊",
        onBack: () {
          Navigator.pop(context);
        },
      ),
      body: Center(
        child: Column(
          children: [
            Expanded(child: ImageSwitchWidget(
                deviationRatio: deviationRatio,
                childWidth: 150,
                childHeight: 150,
                children: [
                  Image.asset(
                    R.image1_webp,
                  ),
                  Image.asset(
                    R.image2_webp,
                  ),
                  Image.asset(
                    R.image3_jpg,
                  ),
                  Image.asset(
                    R.image4_webp,
                  ),
                  Image.asset(
                    R.image5_webp,
                  ),
                  Image.asset(
                    R.image6_webp,
                  ),
                  Image.asset(
                    R.image7_webp,
                  ),
                ]),),
            Slider(
              value: deviationRatio,
              onChanged: (value) {
                setState(() {
                  deviationRatio = value;
                });
              },
            )
          ],
        ),
      ),
    );
  }
}

class ImageSwitchWidget extends StatefulWidget {
  const ImageSwitchWidget({
    Key? key,
    this.children,
    this.childWidth = 80,
    this.childHeight = 80,
    this.deviationRatio = 0.8,
    this.minScale = 0.4,
    this.circleScale = 1,
  }) : super(key: key);

  //所有的子控件
  final List<Widget>? children;

  //每个子控件的宽
  final double childWidth;

  //每个子控件的高
  final double childHeight;

  //偏移X系数  0-1
  final double deviationRatio;

  //最小缩放比 子控件的滑动时最小比例
  final double minScale;

  //圆形缩放系数
  final double circleScale;

  @override
  State<StatefulWidget> createState() => ImageSwitchState();
}

class ImageSwitchState extends State<ImageSwitchWidget>
    with TickerProviderStateMixin {
  //所有子布局的位置信息
  List<Point> childPointList = [];

  //滑动系数
  final slipRatio = 0.5;

  //开始角度
  double startAngle = 0;

  //旋转角度
  double rotateAngle = 0.0;

  //按下时X坐标
  double downX = 0.0;

  //按下时的角度
  double downAngle = 0.0;

  //大小
  late Size size;

  //半径
  double radius = 0.0;

  late AnimationController _controller;
  late Animation<double> animation;

  late double velocityX;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1000),
    );

    animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.linearToEaseOut,
    );

    animation = Tween<double>(begin: 1, end: 0).animate(animation)
      ..addListener(() {
        //当前速度
        var velocity = animation.value * -velocityX;
        var offsetX = radius != 0 ? velocity * 5 / (2 * pi * radius) : velocity;
        rotateAngle += offsetX;
        setState(() => {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {}
      });
  }

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

  ///子控件集
  List<Point> _childPointList({Size size = Size.zero}) {
    size = Size(
      max(widget.childWidth, size.width * widget.circleScale),
      max(widget.childWidth, size.height * widget.circleScale),
    );
    childPointList.clear(); //清空之前的数据
    if (widget.children?.isNotEmpty ?? false) {
      //子控件数量
      int count = widget.children?.length ?? 0;
      //平均角度
      double averageAngle = 360 / count;
      //半径
      radius = size.width / 2 - widget.childWidth / 2;
      for (int i = 0; i < count; i++) {
        //当前子控件的角度
        double angle = startAngle + averageAngle * i - rotateAngle;
        //当前子控件的中心点坐标  x=width/2+sin(a)*R   y=height/2+cos(a)*R
        var centerX = size.width / 2 + sin(radian(angle)) * radius;
        var centerY = size.height / 2 +
            cos(radian(angle)) * radius * cos(pi / 2 * widget.deviationRatio);
        var minScale = min(widget.minScale, 0.99);
        var scale = (1 - minScale) / 2 * (1 + cos(radian(angle - startAngle))) +
            minScale;
        childPointList.add(Point(
          centerX,
          centerY,
          widget.childWidth * scale,
          widget.childHeight * scale,
          centerX - widget.childWidth * scale / 2,
          centerY - widget.childHeight * scale / 2,
          centerX + widget.childWidth * scale / 2,
          centerY + widget.childHeight * scale / 2,
          scale,
          angle,
          i,
        ));
      }
      childPointList.sort((a, b) {
        return a.scale.compareTo(b.scale);
      });
    }
    return childPointList;
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (
      BuildContext context,
      BoxConstraints constraints,
    ) {
      var minSize = min(constraints.maxWidth, constraints.maxHeight);
      size = Size(minSize, minSize);
      return GestureDetector(
        ///水平滑动按下
        onHorizontalDragDown: (DragDownDetails details) {
          _controller.stop();
        },

        ///水平滑动开始
        onHorizontalDragStart: (DragStartDetails details) {
          //记录拖动开始时当前的选择角度值
          downAngle = rotateAngle;
          //记录拖动开始时的x坐标
          downX = details.globalPosition.dx;
        },

        ///水平滑动中
        onHorizontalDragUpdate: (DragUpdateDetails details) {
          //滑动中X坐标值
          var updateX = details.globalPosition.dx;
          //计算当前旋转角度值并刷新
          rotateAngle = (downX - updateX) * slipRatio + downAngle;
          if (mounted) setState(() {});
        },

        ///水平滑动结束
        onHorizontalDragEnd: (DragEndDetails details) {
          //x方向上每秒速度的像素数
          velocityX = details.velocity.pixelsPerSecond.dx;
          _controller.reset();
          _controller.forward();
        },

        ///滑动取消
        onHorizontalDragCancel: () {},
        behavior: HitTestBehavior.opaque,
        child: CustomPaint(
          size: size,
          child: Stack(
            children: _childPointList(size: size).map(
              (Point point) {
                return Positioned(
                  width: point.width,
                  left: point.left,
                  top: point.top,
                  child: Transform(
                    transform: Matrix4.rotationY(radian(point.angle)),
                    alignment: AlignmentDirectional.center,
                    child: Container(
                      decoration: BoxDecoration(
                        boxShadow: [
                          BoxShadow(
                            color: Colors.grey.withOpacity(0.5),
                            blurRadius: 16,
                            offset: const Offset(0, 16),
                          ),
                        ],
                      ),
                      child: widget.children![point.index],
                    ),
                  ),
                );
              },
            ).toList(),
          ),
        ),
      );
    });
  }

  ///角度转弧度
  ///弧度 =度数 * (π / 180)
  ///度数 =弧度 * (180 / π)
  double radian(double angle) {
    return angle * pi / 180;
  }
}

///子控件属性对象
class Point {
  Point(this.centerX, this.centerY, this.width, this.height, this.left,
      this.top, this.right, this.bottom, this.scale, this.angle, this.index);

  double centerX;
  double centerY;
  double width;
  double height;
  double left;
  double top;
  double right;
  double bottom;
  double scale;
  double angle;
  int index;
}

运行看看:
111.gif

详情github : github.com/yixiaolunhui/flutter_xy

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【活动】政府工作报告视角下的计算机行业发展前瞻与策略探讨

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 引言正文计算机行业在政府工作报告中的定位与发展态势政策导向解析未来机遇展望…

【uniapp】表单验证不生效的解决方案

表单验证这个常见的功能&#xff0c;明明在element ui等框架已经用的很熟了&#xff0c;在uniapp开发时还是处处碰壁&#xff1f;这篇文章我会提示uni-forms表单验证的几个注意点&#xff0c;帮助大家排查。 示例 下面是一份包含普通验证和自定义验证的示例&#xff1a; <…

vsto excel 插件注册表属性值含义

在 VSTO (Visual Studio Tools for Office) 中&#xff0c;LoadBehavior 是用于指定 Office 插件加载行为的一个属性。具体含义如下&#xff1a; - LoadBehavior 0&#xff1a;此值表示插件已被禁用&#xff0c;将不会加载。 - LoadBehavior 1&#xff1a;此值表示插件将在 O…

Sentinel基础使用

1. 概念解释 限流&#xff1a;对并发访问进行限速。限流的一些行为&#xff1a; 1. 拒绝服务&#xff1a;将多余的请求直接拒绝掉2.服务降级&#xff1a;降级甚至关闭后台的某些服务3.特权请求&#xff1a;在多租户或者对用户进行分级时&#xff0c;考虑让特权用户进行访问4.延…

osgEarth学习笔记2-第一个Osg QT程序

原文链接 上个帖子介绍了osgEarth开发环境的安装。本帖介绍我的第一个Osg QT程序。 下载 https://github.com/openscenegraph/osgQt 解压&#xff0c;建立build目录。 使用Cmake-GUI Configure 根据需要选择win32或者x64&#xff0c;这里我使用win32. 可以看到include和lib路…

C语言数据结构与算法笔记(排序算法)

排序算法 基础排序 冒泡排序 核心为交换&#xff0c;通过不断进行交换&#xff0c;将大的元素一点一点往后移&#xff0c;每一轮最大的元素排到对应的位置上&#xff0c;形成有序。 设数组长度为N&#xff0c;过程为: 共进行N轮排序每一轮排序从数组的最左边开始&#xff0…

云原生(三)、Docker网络

Docker网络 在 Docker 中&#xff0c;不同容器之间的网络访问原理取决于容器所使用的网络模式。下面是 Docker 中常见的两种网络模式下容器间网络访问的原理&#xff1a; 桥接模式&#xff08;Bridge&#xff09;&#xff1a; 在桥接模式下&#xff0c;Docker 使用 Linux 桥接…

Vue3学习日记 Day4 —— pnpm,Eslint

注&#xff1a;此课程需要有Git的基础才能学习 一、pnpm包管理工具 1、使用原因 1.1、速度快&#xff0c;远胜过yarn和npm 1.2、节省磁盘空间 2、使用方式 2.1、安装方式 npm install -g pnpm 2.2、创建项目 pnpm create vue 二、Eslint配置代码风格 1、环境同步 1、禁用Pret…

Jenkins实现CICD(3)_Jenkins连接到git

文章目录 1、如何完成上述操作&#xff0c;并且不报如下错&#xff1a;2、连接不上git&#xff0c;操作如下&#xff1a;3、将上边产生的3个文件拷贝到&#xff1a;C:\Windows\System32\config\systemprofile\.ssh4、新建下图凭证&#xff1a;创建步骤&#xff1a; 5、公钥填到…

OpenCV 新版滴 4.5.1 发布啦!

发布亮点&#xff1a; OpenCV Github 项目终于突破50000 stars&#xff01;新的里程碑~ 这次发布的特性包括&#xff1a; 集成更多的GSoC 2020 项目的结果&#xff0c;包括&#xff1a; 开发了OpenCV.js DNN 模块&#xff0c;以方便再网页中使用&#xff0c;并提供了相关教…

基于yolov2深度学习网络的人脸检测matlab仿真,图像来自UMass数据集

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 网络架构与特征提取 4.2 输出表示 4.3损失函数设计 4.4预测阶段 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 load yolov2.mat% 加载…

浏览器架构的温故知新

【引子】前端可能是一个日新月异的领域&#xff0c;我们很难了解其中的方方面面。但是&#xff0c;前端系统一般都以浏览器作为运行环境&#xff0c; 对浏览器的进一步理解有助于我们更好地开发前端应用。这也是本文的由来之一&#xff0c;也作为对runtime的一次实例分析。 浏览…

全国各省市县统计年鉴/中国环境统计年鉴/中国工业企业数据库/中国专利数据库/污染排放数据库

统计年鉴是指以统计图表和分析说明为主&#xff0c;通过高度密集的统计数据来全面、系统、连续地记录年度经济、社会等各方面发展情况的大型工具书来获取统计数据资料。 统计年鉴是进行各项经济、社会研究的必要前提。而借助于统计年鉴&#xff0c;则是研究者常用的途径。目前国…

Web3 之力:探索去中心化技术的创新应用

在当今数字化时代&#xff0c;随着区块链技术的发展和应用&#xff0c;Web3作为其重要组成部分&#xff0c;正在逐渐改变着我们对于互联网和数字经济的认知与体验。Web3不仅是一种技术革新&#xff0c;更是一种新的思维范式&#xff0c;其去中心化的特点为数字世界带来了更多的…

DOcker搭建Rancher

简介 Rancher 是供采用容器的团队使用的完整软件堆栈。它解决了管理多个Kubernetes集群的运营和安全挑战&#xff0c;并为DevOps团队提供用于运行容器化工作负载的集成工具。 官网地址&#xff1a;https://www.rancher.cn/ 安装 拉取镜像 docker pull rancher/rancher:stab…

Chrome历史版本下载地址:Google Chrome Older Versions Download (Windows, Linux Mac)

最近升级到最新版本Chrome后发现页面居然显示错乱,是在无语, 打算退回原来的版本, 又发现官方只提供最新的版本下载, 为了解决这个问题所有收集了Chrome历史版本的下载地址分享给大家. Google Chrome Windows version 32-bit VersionSizeDate104.0.5112.10279.68 MB2022-05-30…

汽车电子拓扑架构的演进过程

汽车电子拓扑架构的演进过程 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师 (Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,喝完再挣扎,出门靠…

【Sass】1px分割线 + 缩进分割线

效果图 1. 亮色模式效果 2. 暗色模式效果 设计思路 配色使用grey色 优点&#xff1a;无论在暗色模式还是亮色模式都可以看清楚分割线 使用after,before 伪元素绘制线条&#xff0c;并压缩线条transform: scaleY(.25) 注意事项 必须确保父级有宽高父级定位必须为position: r…

【地图】腾讯地图 - InfoWindow 自定义信息窗口内容时,内容 html 嵌套混乱问题

目录 需求描述问题问题代码页面展示 解决原因解决办法解决代码页面展示 代码汇总注 需求描述 腾讯地图上画点位&#xff0c;点击点位展示弹框信息 问题 问题代码 // 打开弹框 openInfoWindow(position, content) {this.infoWindow new TMap.InfoWindow({map: this.map,posit…

使用jenkins-pipeline进行利用项目文件自动化部署到k8s上

Discard old builds:丢弃旧的构建,目的是管理存储空间、提升性能以及保持环境整洁 Do not allow concurrent builds: 禁止并发构建是指同一时间内只允许一个构建任务执行,避免多个构建同时运行可能带来的问题 Do not allow the pipeline to resume if the controller resta…