Flutter 113: 图解自定义 ACEPieWidget 饼状图 (二)

简介: 0 基础学习 Flutter,第一百一十三步:继续完善自定义 ACEPieWidget 饼状图 (二) 添加基本手势操作!

    小菜上一节尝试绘制了一个简单的饼状图,今天尝试添加一点手势操作,可以随手指旋转饼状图;

ACEPieWidget

Gesture

    小菜在之前绘制好的饼状图基础上添加一个简单的旋转手势操作;

1. 手势范围

    小菜习惯重写 PanGestureRecognizer 来对手势操作进行监听,也可以直接通过 Gesture 来直接处理;

return Container(
    width: double.infinity,
    height: double.infinity,
    child: RawGestureDetector(
        child: CustomPaint(
            key: _key,
            painter: PiePainter(widget.listData, this.rotateAngle)),
        gestures: <Type, GestureRecognizerFactory>{
          ACEPieGestureRecognizer:
              GestureRecognizerFactoryWithHandlers<ACEPieGestureRecognizer>(
                  () => ACEPieGestureRecognizer(), (ACEPieGestureRecognizer gesture) {
            gesture.onDown = (detail) {
              
            };
            gesture.onUpdate = (detail) {
              
            };
            gesture.onEnd = (detail) {
             
            };
          })
        }));

2. 计算旋转角度

    小菜预计的想法是,通过 gesture.onUpdate 更新手势坐标,与初始坐标差来定位旋转角度;其中饼状图绘制是采用的笛卡尔坐标系,以左上角为坐标系原点;而居中的饼状图圆心是在整个组件所在的屏幕尺寸中心;

RenderBox box = _key.currentContext.findRenderObject();
Offset offset = box.localToGlobal(Offset.zero);
Offset _centerOffset = Offset(offset.dx + box.size.width * 0.5, offset.dy + box.size.height * 0.5);

    小菜采用通用 RenderBox 的方式获取自定义 ACEPieWidget 所占屏幕尺寸并获取饼状图圆心坐标;

    其中需要注意的是手势监听的 Offset details 获取坐标方式略有不同:detail.localPosition 获取的是当前组建内相对于左上角坐标原点的相对位置,而 detail.globalPosition 获取的是整个设备屏幕左上角坐标的实际位置,小菜刚开始通过 localPosition 方式获取,计算得出的角度受 Widget 所占位置及尺寸影响,差别较大,建议使用 globalPosition 方式;

    通过 gesture.onUpdate 更新后的坐标点与更新前的坐标点,再结合饼状图圆心坐标,三点确定一个三角形,通过余弦定律获取手势操作的夹角,从而重新绘制饼状图;

_rotateAngle() {
  var _onDownLen = sqrt(pow(_startOffset.dx - _centerOffset.dx, 2) +
      pow(_startOffset.dy - _centerOffset.dy, 2));
  var _onUpdateLen = sqrt(pow(_updateOffset.dx - _centerOffset.dx, 2) +
      pow(_updateOffset.dy - _centerOffset.dy, 2));
  var _downToUpdateLen = sqrt(pow((_startOffset.dx - _updateOffset.dx), 2) +
      pow((_startOffset.dy - _updateOffset.dy), 2));
  var _cosAngle = (_onDownLen * _onDownLen + _onUpdateLen * _onUpdateLen -
          _downToUpdateLen * _downToUpdateLen) / (2 * _onDownLen * _onUpdateLen);
  rotateAngle += acos(_cosAngle);
  setState(() {});
}

3. 旋转方向

    小菜通过上述方式获取三角形角度后发现旋转的方向只能是顺时针旋转,反向的逆时针手势缺未生效;其原因是通过余弦定律转换的角度都为正数,需要通过向量方式进行方向正负的判断;于是小菜更换了另一种方式,以饼状图圆心为坐标轴原点,水平向右设置一个单位向量,再通过前后手势变更的坐标进行计算两个角度,相差即是夹角;

_rotateAngle() {
  if (_startOffset.dy < _centerOffset.dy) {
    gestureDirection = -1;
  } else {
    gestureDirection = 1;
  }
  var _updateAngle = gestureDirection *
      _angle(_updateOffset, Offset(_centerOffset.dx + 100, _centerOffset.dy), _centerOffset);
  if (_updateOffset.dy < _centerOffset.dy) {
    gestureDirection = -1;
  } else {
    gestureDirection = 1;
  }
  var _startAngle = gestureDirection *
      _angle(_startOffset, Offset(_centerOffset.dx + 100, _centerOffset.dy), _centerOffset);
  return (_updateAngle - _startAngle);
}

_angle(_aPoint, _bPoint, _oPoint) {
  var _oALen = sqrt(pow(_aPoint.dx - _oPoint.dx, 2) + pow(_aPoint.dy - _oPoint.dy, 2));
  var _oBLen = sqrt(pow(_bPoint.dx - _oPoint.dx, 2) + pow(_bPoint.dy - _oPoint.dy, 2));
  var _aBLen = sqrt(pow(_aPoint.dx - _bPoint.dx, 2) + pow(_aPoint.dy - _bPoint.dy, 2));
  var _cosAngle = (pow(_oALen, 2) + pow(_oBLen, 2) - pow(_aBLen, 2)) /
      (2 * _oALen * _oBLen);
  return acos(_cosAngle);
}

    其中在计算的时候用到一些基本的数学函数公式,之后小菜会简单介绍一下 dart:math 函数库;计算所得的角度加在饼状图遍历绘制的扇形图角度中即可;其中注意在文字绘制时也要注意旋转坐标系角度;

if (_listData != null) {
  for (int i = 0; i < _listData.length; i++) {
    startAngle += sweepAngle;
    sweepAngle = _listData[i].values.first * 2 * pi / _sum;
    canvas.drawArc(_circle, startAngle + _rotateAngle, sweepAngle, true,
        _paint..color = _subPaint(_listData[i].keys.first));
    if (sweepAngle >= pi / 6) {
      canvas.translate(size.width * 0.5, size.height * 0.5);
      canvas.rotate(startAngle + sweepAngle * 0.5 + _rotateAngle);
      Paragraph paragraph = (_pb..addText(_subName)).build()..layout(_paragraph);
      canvas.drawParagraph(paragraph, Offset(50.0, 0.0 - paragraph.height * 0.5));
      canvas.rotate(-startAngle - sweepAngle * 0.5 - _rotateAngle);
      canvas.translate(-size.width * 0.5, -size.height * 0.5);
    }
  }
}

dart:math

    小菜在绘制饼状图过程中需要使用三角函数等进行偏移量绘制,此时需要一些基础的数学计算;而 Dart 也有简单的 dart:math 库,主要用来数学常数和函数使用,以及随机数生成器等;

1. 常量数据

    dart:math 提供了我们日常用的自然数底数 e、对数 ln 以及圆周率 pi 等,精确了很多位,避免我们自己定义;

// 自然对数的底数 e
'e -> $e';
// 以 e 为底 10 的对数
'ln10 -> $ln10';
// 以 e 为底 2 的对数
'ln2 -> $ln2';
// 以 2 为底 e 的对数
'log2e -> $log2e';
// 以 10 为底 e 的对数
'log10e -> $log10e';
// 圆周率
'pi -> $pi';
// 2 的平方根
'sqrt2 -> $sqrt2';
// 1/2 的平方根
'sqrt2 -> $sqrt2';

2. 倍数/指数函数

    dart:math 提供了平方根,求幂,指数函数等便利的函数方法;

// 平方根
double sqrt(num x);
// 自然指数 e 的 x 次幂
double exp(num x);
// 自然数 x 的对数
double log(num x);
// 最小值比较
T min<T extends num>(T a, T b);
// 最大值比较
T max<T extends num>(T a, T b);
// x 的 y 次幂
num pow(num x, num exponent);

3. 三角函数

    对于三角函数,提供了弧度转为角度的正弦/余弦/正切函数,同样提供了由角度值转为弧度值转换方法,需要注意例如负数、0、无穷数、无理数等特殊场景;

// 正弦函数
double sin(num radians);
// 余弦函数
double cos(num radians);
// 正切函数
double tan(num radians);
// 弧度转为正弦值
double asin(num x);
// 弧度转为余弦值
double acos(num x);
// 弧度转为正切值
double atan(num x);


    ACEPieWidget 案例源码

    dart:math 案例源码


来源: 阿策小和尚
目录
相关文章
|
定位技术 开发工具 开发者
为了让外卖小哥在地图里开上火箭🚀我用FLutter自定义了地图
花了五天时间,用Flutter自定义地图是什么体验?外卖小哥都开上火箭了?什么?我被女朋友赶出家门啦?欢迎观看被女友赶出家门之开火箭送外卖篇~
|
4天前
|
前端开发 搜索推荐 开发者
【Flutter前端技术开发专栏】Flutter中的自定义主题与暗黑模式
【4月更文挑战第30天】本文介绍了如何在Flutter中自定义主题和实现暗黑模式。通过`ThemeData`类定义应用的外观,包括颜色、字体和样式。示例展示了如何设置主色、强调色及文本、按钮主题。暗黑模式可通过`darkTheme`属性启用,结合`ThemeData.dark()`方法定制。利用`MediaQuery`监听系统亮度变化,动态调整暗黑模式状态。Flutter的这些特性有助于开发者创建独特且用户友好的界面。
【Flutter前端技术开发专栏】Flutter中的自定义主题与暗黑模式
|
4天前
|
缓存 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义绘制与Canvas API
【4月更文挑战第30天】Flutter允许开发者通过`CustomPaint`和`CustomPainter`进行自定义绘制,以实现丰富视觉效果。`CustomPaint` widget将`CustomPainter`应用到画布,而`CustomPainter`需实现`paint`和`shouldRepaint`方法。`paint`用于绘制图形,如示例中创建的`MyCirclePainter`绘制蓝色圆圈。Canvas API提供绘制形状、路径、文本和图片等功能。注意性能优化,避免不必要的重绘和利用缓存提升效率。自定义绘制让Flutter UI更具灵活性和个性化,但也需要图形学知识和性能意识。
【Flutter前端技术开发专栏】Flutter中的自定义绘制与Canvas API
|
4天前
|
开发框架 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
【4月更文挑战第30天】探索Flutter的自定义Widget与渲染流程。自定义Widget是实现复杂UI设计的关键,优点在于个性化设计、功能扩展和代码复用,但也面临性能优化和复杂性管理的挑战。创建步骤包括设计结构、定义Widget类、实现构建逻辑和处理交互。Flutter渲染流程涉及渲染对象树、布局、绘制和合成阶段。实践案例展示如何创建带渐变背景和阴影的自定义按钮。了解这些知识能提升应用体验并应对开发挑战。查阅官方文档以深入学习。
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
|
4天前
|
前端开发 开发者 UED
Flutter的自定义Painter:深入探索自定义绘制Widget的技术实现
【4月更文挑战第26天】Flutter的自定义Painter允许开发者根据需求绘制独特UI,通过继承`CustomPaint`类和重写`paint`方法实现。在`paint`中使用`Canvas`绘制图形、路径等。创建自定义Painter类后,将其作为`CustomPaint` Widget的`painter`属性使用。此技术可实现自定义形状、渐变、动画等复杂效果,提升应用视觉体验。随着Flutter的进化,自定义Painter将提供更丰富的功能。
|
4天前
|
运维 监控 定位技术
应用研发平台EMAS常见问题之flutter插件不支持自定义图标如何解决
应用研发平台EMAS(Enterprise Mobile Application Service)是阿里云提供的一个全栈移动应用开发平台,集成了应用开发、测试、部署、监控和运营服务;本合集旨在总结EMAS产品在应用开发和运维过程中的常见问题及解决方案,助力开发者和企业高效解决技术难题,加速移动应用的上线和稳定运行。
81 0
|
4天前
Flutter 自定义ICON库
Flutter 自定义ICON库 Flutter提供了一些内置的ICON库,但在实际开发中,可能需要一些自定义的ICON图标。Flutter允许我们使用自定义图标,本文将介绍如何创建和使用自定义ICON库。
|
4天前
|
UED
Flutter之自定义路由切换动画
Flutter之自定义路由切换动画 在Flutter中,我们可以通过Navigator来实现路由管理,包括路由的跳转和返回等。默认情况下,Flutter提供了一些简单的路由切换动画,但是有时候我们需要自定义一些特殊的动画效果来提高用户体验。本文将介绍如何在Flutter中实现自定义的路由切换动画。
|
4天前
|
开发框架 Dart 容器
Flutter 自定义渐变按钮 GradientButton
Flutter 自定义渐变按钮 GradientButton Flutter 是一种流行的跨平台移动应用开发框架。Flutter 提供了许多内置的小部件,但有时您可能需要创建自己的小部件以满足特定的需求。这个文档将介绍如何创建一个自定义渐变按钮小部件 GradientButton。
|
4天前
|
容器
Flutter 自定义实现时间轴、侧边进度条
时间轴和侧边进度条是非常常见的 UI 控件,它们可以增强应用的视觉效果和交互体验。在这篇文章中,我们将详细介绍如何使用 Flutter 自定义实现这两个控件。
111 1