flutter 画转盘

news2024/12/28 23:21:51
import 'package:flutter/material.dart';
import 'dart:math';

const double spacingAngle = 45.0; // 每两个文字之间的角度
// 自定义绘制器,ArcTextPainter 用于在圆弧上绘制文字
class ArcTextPainter extends CustomPainter {
  final double rotationAngle; // 动画旋转角度
  final double strokeWidth; // 圆环的宽度
  final List<String> text; // 文字列表
  final double curIndex; // 当前旋转进度


  ArcTextPainter({
    required this.rotationAngle,
    required this.strokeWidth,
    required this.text,
    required this.curIndex,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final radius = size.width / 2; // 圆的半径
    final center = Offset(size.width / 2, size.height / 2); // 圆心的坐标

    // 创建用于绘制圆弧的画笔
    final paint = Paint()
      ..color = Colors.grey.shade300 // 圆弧的颜色
      ..strokeWidth = strokeWidth // 圆弧的宽度
      ..style = PaintingStyle.stroke; // 画笔样式为描边

    // 计算圆弧的矩形区域
    final arcRect = Rect.fromCircle(center: center, radius: radius - strokeWidth / 2);
    canvas.drawArc(arcRect, pi, pi, false, paint); // 绘制圆弧

    // 创建用于绘制箭头的画笔
    final arrowPaint = Paint()
      ..color = Colors.purple // 箭头的颜色
      ..style = PaintingStyle.fill; // 画笔样式为填充

    // 定义箭头的路径
    final arrowPath = Path();
    arrowPath.moveTo(center.dx, center.dy - radius + strokeWidth / 2); // 箭头起点
    arrowPath.lineTo(center.dx - 10, center.dy - radius + strokeWidth / 2 + 20); // 箭头的左边
    arrowPath.lineTo(center.dx + 10, center.dy - radius + strokeWidth / 2 + 20); // 箭头的右边
    arrowPath.close(); // 结束路径

    canvas.drawPath(arrowPath, arrowPaint); // 绘制箭头

    // 绘制圆弧上的文字
    _drawTextAlongArc(canvas, center, radius - strokeWidth / 2);
  }

  // 在圆弧上绘制文字
  void _drawTextAlongArc(Canvas canvas, Offset center, double radius) {
    final textPainter = TextPainter(
      textAlign: TextAlign.center, // 文字对齐方式为居中
      textDirection: TextDirection.ltr, // 文字方向为从左到右
    );

    // 遍历所有文字并绘制
    for (int i = 0; i < text.length; i++) {
      // 计算当前文字的角度
      double angle = (i - curIndex) * spacingAngle * (pi / 180) - pi/2;

      // print("angle:${i} ${angle*180/pi}");

      // 检查文字是否在可视范围内
      if (angle >= -pi && angle <= 0) {
        // 计算文字的位置
        final x = center.dx + radius * cos(angle); // x 坐标
        final y = center.dy + radius * sin(angle); // y 坐标

        canvas.save(); // 保存当前画布状态
        canvas.translate(x, y); // 移动画布到文字的位置

        // 设置文字的样式和内容
        textPainter.text = TextSpan(
          text: text[i],
          style: TextStyle(fontSize: 14, color: Colors.black), // 文字的样式
        );
        textPainter.layout(); // 计算文字的大小

        // 计算文字的实际可见区域
        double visibleFraction = _calculateVisibleFraction(angle);
        if (visibleFraction < 1.0) {
          // 如果文字不完全可见,则应用裁剪遮罩
          canvas.clipRect(Rect.fromLTWH(
            -textPainter.width / 2, // 左上角 x 坐标
            -textPainter.height / 2, // 左上角 y 坐标
            textPainter.width, // 文字的宽度
            textPainter.height, // 文字的高度
          ));
        }

        textPainter.paint(canvas, Offset(-textPainter.width / 2, -textPainter.height / 2)); // 绘制文字
        canvas.restore(); // 恢复画布状态
      }
    }
  }

  // 计算文字的可见比例
  double _calculateVisibleFraction(double angle) {
    // 文字显示的比例,确保在 [-pi, 0] 范围内显示完全
    if (angle < -pi / 2) {
      return max(0, (angle + pi) / (pi / 2)); // 文字被遮挡的部分
    } else if (angle > 0) {
      return max(0, (-angle) / (pi / 2)); // 文字被遮挡的部分
    }
    return 1.0; // 文字完全可见
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true; // 是否需要重新绘制
}

// ArcTextExample 是一个示例 widget,用于展示自定义绘制的效果
class ArcTextExample extends StatefulWidget {
  final double strokeWidth; // 圆环的宽度
  final List<String> text; // 文字列表
  final int initialIndex; // 初始索引
  final double animationDuration; // 动画持续时间

  const ArcTextExample({
    Key? key,
    required this.strokeWidth,
    required this.text,
    required this.initialIndex,
    required this.animationDuration,
  }) : super(key: key);

  @override
  _ArcTextExampleState createState() => _ArcTextExampleState();
}

class _ArcTextExampleState extends State<ArcTextExample>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller; // 动画控制器
  late Animation<double> _animation; // 动画
  double curIndex = 0.0; // 保存当前旋转的进度
  bool isAnimating = false; // 标记动画是否正在进行
  final TextEditingController indexController = TextEditingController(); // 目标索引的文本控制器
  final TextEditingController durationController = TextEditingController(); // 动画持续时间的文本控制器

  @override
  void initState() {
    super.initState();

    // 初始化文本控制器的值
    indexController.text = widget.initialIndex.toString();
    durationController.text = widget.animationDuration.toString();

    // 计算初始旋转角度
    double initialAngle = ( - widget.initialIndex ) * spacingAngle * (pi / 180) - pi / 2;
    curIndex = widget.initialIndex.toDouble(); // 初始化时 curIndex 是初始索引

    // 创建动画控制器
    _controller = AnimationController(
      duration: Duration(seconds: widget.animationDuration.toInt()), // 设置动画的持续时间
      vsync: this, // 与当前的 TickerProvider 绑定
    );

    // print("initialAngle: ${initialAngle*180/pi}");

    // 创建动画
    _animation = Tween<double>(
      begin: initialAngle, // 动画开始的角度
      end: initialAngle + 2 * pi, // 动画结束的角度
    ).animate(_controller)
      ..addListener(() {
        setState(() {
          print("_animation.value:  ${_animation.value * 180 / pi}");
          // 更新当前角度对应的索引范
          curIndex = (-(_animation.value + pi / 2) * (180 / pi)) / spacingAngle;
          print("Current Index: ${curIndex.toStringAsFixed(2)}"); // 打印当前索引
        });
      });
  }

  @override
  void dispose() {
    _controller.dispose(); // 释放动画控制器资源
    indexController.dispose(); // 释放目标索引的文本控制器资源
    durationController.dispose(); // 释放动画持续时间的文本控制器资源
    super.dispose();
  }

  // 旋转到目标索引
  void rotateToIndex(int targetIndex, double duration) {
    if(targetIndex != curIndex){
      setState(() {
        if (isAnimating) {
          // 如果正在进行动画,则停止并重置
          _controller.stop();
          isAnimating = false;
        }

        _controller.duration = Duration(seconds: duration.toInt()); // 设置动画的持续时间

        double startAngle = (-curIndex) * spacingAngle * (pi / 180) - pi / 2; // 使用当前索引角度作为起始角度
        double targetAngle = (-targetIndex) * spacingAngle * (pi / 180) - pi / 2;  // 计算目标角度
        print("开始度数: ${startAngle * 180/pi} 结束度数:${targetAngle * 180/pi}");
        double endAngle;
        // 确定旋转方1
        if (targetAngle < 0) {
          // 顺时针旋转
          endAngle = startAngle + targetAngle;
        } else {
          // 逆时针旋转
          endAngle = startAngle - targetAngle;
        }

        _animation = Tween<double>(
          begin: startAngle, // 动画开始的角度
          end: targetAngle, // 动画结束的角度
        ).animate(_controller);

        isAnimating = true; // 标记动画为进行中
        _controller.reset(); // 重置动画控制
        _controller.forward(); // 开始动画
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          SizedBox(height: 140), // 上部间距
          Center(
            child: CustomPaint(
              size: Size(300, 200), // 设置圆弧的大小
              painter: ArcTextPainter(
                rotationAngle: _animation.value, // 当前旋转角度
                strokeWidth: widget.strokeWidth, // 圆环的宽度
                text: widget.text, // 文字列表
                curIndex: curIndex, // 当前旋转进度
              ),
            ),
          ),
          SizedBox(height: 20), // 下部间距
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 20.0), // 水平内边距
            child: Column(
              children: [
                // 目标索引的输入框
                TextField(
                  controller: indexController,
                  decoration: InputDecoration(
                    labelText: 'Target Index', // 输入框标签
                  ),
                  keyboardType: TextInputType.number, // 键盘类型为数字
                ),
                // 动画持续时间的输入框
                TextField(
                  controller: durationController,
                  decoration: InputDecoration(
                    labelText: 'Animation Duration (seconds)', // 输入框标签
                  ),
                  keyboardType: TextInputType.number, // 键盘类型为数字
                ),
                SizedBox(height: 20), // 输入框和按钮之间的间距
                // 旋转按钮
                ElevatedButton(
                  onPressed: () {
                    // 获取目标索引和动画持续时间
                    int targetIndex = int.tryParse(indexController.text) ?? 0;
                    double duration = double.tryParse(durationController.text) ?? 10.0;

                    // if (isAnimating) {
                    //   // 如果动画正在进行,停止并保存当前进度
                    //   _controller.stop();
                    //   curIndex = (-(_animation.value + pi / 2) * (180 / pi)) / spacingAngle; // 保存当前进度为 curIndex
                    //   _controller.reset(); // 重置动画控制器
                    // }

                    // 旋转到目标索引
                    rotateToIndex(targetIndex, duration);
                  },
                  child: Text('Rotate to Index'), // 按钮文本
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: ArcTextExample(
      strokeWidth: 100.0, // 圆环的宽度
      text: List.generate(11, (i) => '第$i层'), // 文字列表
      initialIndex: 3, // 初
      animationDuration: 10.0, // 默认动画时间为10秒
    ),
  ));
}

​​​​​​​

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

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

相关文章

多种办公功能的WORD VBA程序

word的VBA办公助手 源代码 Option Explicit 需要引用 excel 16.0 库 所有内容仅供个人学习使用&#xff0c;严禁传播。1-公共变量-表格属性------------------------------------------------------------------------- Dim Hg% hg:行高 Const K1 0.1 Dim Flg_bh As Boolean …

力扣题/二叉树/二叉树中的最大路径和

二叉树中的最大路径和 力扣原题 二叉树中的 路径 被定义为一条节点序列&#xff0c;序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点&#xff0c;且不一定经过根节点。路径和 是路径中各节点值的总和。给你一个二叉树…

景联文科技:图像标注的类型有哪些?

图像标注是计算机视觉领域中一个非常重要的步骤&#xff0c;它是创建训练数据集的关键组成部分&#xff0c;主要用于帮助机器学习算法理解图像内容。 以下是图像标注的一些主要类型&#xff1a; 1. 边界框标注&#xff1a; • 这是最常见的标注方式之一&#xff0c;通常用于…

Python深度学习框架库之caffe使用详解

概要 Caffe 是一个由伯克利视觉与学习中心(BVLC)开发的深度学习框架,以其速度快、模块化设计和社区支持而闻名。Caffe 适用于视觉识别任务,广泛应用于学术研究和产业实践中。Caffe 提供了一个强大的 Python 接口,使开发者能够方便地使用 Python 进行深度学习模型的开发和…

QT 简易音乐播放器

目录 放置控件 获取mp3文件 播放音乐 准备工作 加载模块 加载头文件 new一个output对象,Mediaplayer对象 把outpout对象交给mediaplayer对象 给播放器设置音乐 播放 优化 上一曲下一曲功能 双击歌曲播放 获取音乐时长和音乐播放时间 让音乐进度条跟随音乐走 调…

解决element-ui回车键绑定按钮功能后却刷新浏览器的问题

最近写代码时&#xff0c;遇到要给回车键绑定确定的功能&#xff0c;并且打开对话框时要自动获取输入框焦点&#xff0c;发现一但重新打开浏览器&#xff0c;第一次执行回车键的功能时就会刷新浏览器&#xff0c;后续则会成功执行。但是一但再一次重新打开浏览器&#xff0c;还…

【简历】北京某985大学:JAVA秋招简历指导,面试通过率较高

注&#xff1a;为保证用户信息安全&#xff0c;姓名和学校等信息已经进行同层次变更&#xff0c;内容部分细节也进行了部分隐藏 简历说明 我们今天要看一位来自25届985同学的JAVA简历。 既然要参加校招的话&#xff0c;我们校招法典的第一准则&#xff1a;定你的学校层次。 …

win11中vsstudio2019使用winusb开usb应用

根据微软官方文档&#xff0c;在安装使用winusb之前&#xff0c;需求wdk环境。 下载 Windows 驱动程序工具包 (WDK) - Windows drivers | Microsoft Learn WDK环境安装 访问安装官网Previous WDK versions and other downloads - Windows drivers | Microsoft Learn 点开vs…

鸿蒙「TaskPool|Worker」多线程并发使用详解,这一篇足够!

概念介绍 鸿蒙的多线程并发TaskPool和Worker&#xff0c;他们具有相同内存模型&#xff0c;线程间隔离内存不共享。在项目中若使用到&#xff0c;有几个较重要的条件或特点这里简单作出列举。 CPU密集型任务&#xff0c;说白了是计算型耗时任务&#xff1b; I/O密集型任务&…

C# 静态方法和实例方法

一、静态成员&#xff0c;实例成员&#xff0c;静态方法&#xff0c;实例方法 静态成员就是用static修饰的字段&#xff1b; 实例成员就是没有被static修饰的字段&#xff1b; 静态方法就是用static修饰的方法&#xff1b; 实例方法就是没有被static修饰的方法&#xff1b;…

OriginPro快速上手指南:数据可视化与分析的利器

目录 OriginLab - Origin and OriginPro - Data Analysis and Graphing Softwarehttps://www.originlab.com/​编辑 一、安装与界面概览 安装 界面概览 二、基础操作 数据输入 创建图表 三、高级功能 数据分析 自动化与脚本 Origin 提供了几个小工具 四、技巧与提示…

AI编程-vscode安装“通义灵码”

“通义灵码”是一款基于阿里云通义代码大模型打造的智能编码助手 1、vscode中&#xff0c;选择插件&#xff0c;输入“tongyi” &#xff0c;弹出插件选项 2、点击install 安装 3、弹出登录提示 4、选择log in&#xff0c;弹出阿里云登录界面 登录成功后提示 5、返回vscode…

【吸引力法则】人生欲:追求深度体验与宇宙链接

文章目录 探究人生欲&#xff1a;追求深度体验与宇宙链接唤醒人生欲&#xff1a;克服配得感的三大障碍法执的压制家庭的继承 探究人生欲&#xff1a;追求深度体验与宇宙链接 在人生的广阔舞台上&#xff0c;我们时常探寻着那些能够引领我们走向更深层次成长与体验的力量。今天&…

C语言—函数递归

一、递归概念 递归其实是⼀种解决问题的⽅法&#xff0c;在C语⾔中&#xff0c;递归就是函数⾃⼰调⽤⾃⼰。下面举一个例子&#xff1a; 上述就是⼀个简单的递归程序&#xff0c;只不过上⾯的递归只是为了演⽰递归的基本形式&#xff0c;不是为了解决问题&#xff0c;代码最终…

ddos造成服务器瘫痪后怎么办

在服务器遭受DDoS攻击后&#xff0c;应立即采取相应措施&#xff0c;包括加强服务器安全、使用CDN和DDoS防御服务来减轻攻击的影响。rak小编为您整理发布ddos造成服务器瘫痪后怎么办。 当DDoS攻击发生时&#xff0c;首先要做的是清理恶意流量。可以通过云服务提供商提供的防护措…

java解析facebook的android app直投下的Referral URL

背景&#xff1a; 在facebook的应用推广中&#xff0c;一般使用两种方式&#xff0c;一种是app直投&#xff0c;一种是w2a。 app直投就是用户点击广告直接跳转到应用商店进行下载应用 w2a就是通过落地页方式引导用户进行应用下载 在w2a模式下&#xff0c;可以通过落地页链接…

Repeat方法:取模运算教材与Unity控制台输出数值不同的原因

学习该知识点的参考教材&#xff1a;Unity API解析/陈宏泉编著.——北京&#xff1a;人民邮电出版社&#xff0c;2014.9 编辑脚本的环境&#xff1a;Visual Studio 2022 在学习该本教材的第五章Mathf类的内容&#xff0c;通过跟随教材上的代码了解不同UnityAPI的具体用法时&a…

【数据结构】七、查找:1.查找的概念、线性结构查找(顺序、折半(二分)、插值、稠密、分块、倒排)

一、查找Search 文章目录 一、查找Search1.查找的基本概念1.1基本概念1.2算法评价标准 二、线性结构1.顺序表查找❗1.1顺序查找1.1.1算法思想1.1.2顺序查找效率分析 2.有序表查找❗2.1折半查找2.1.1算法思想2.1.2判定树构造2.1.3通过判定树进行查找效率分析2.1.4被查找概率不相…

ReactNative笔记(自用)

环境 ios更换gem镜像源&#xff1a; 查看当前源: gem sources -l 移除默认源: gem sources --remove https://rubygems.org/。添加新的源: 添加 Ruby China 的镜像源&#xff1a; gem source -a https://gems.ruby-china.com/或者添加其他镜像源。 清华大学的gem源: htt…

进阶-1.存储引擎

存储引擎 存储引擎1.MySQL体系结构2.存储引擎简介3.存储引擎特点3.1 InnoDB3.2 MyISAM3.3 Memory 4. 存储引擎选择 存储引擎 1.MySQL体系结构 2.存储引擎简介 存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现。存储引擎是基于表的&#xff0c;而不是基于库的&…