首页 > 其他分享 >Flutter 绘制探索 | 扇形区域与点击校验

Flutter 绘制探索 | 扇形区域与点击校验

时间:2023-06-19 12:07:18浏览次数:42  
标签:double 校验 shape 扇形 Offset Flutter SectorShape


Flutter 绘制探索 | 扇形区域与点击校验_架构师

作者:张风捷特烈

0. 前言

今天来探索一个问题,如何绘制一块扇形区域路径,并且校验触点是否落在 扇形区域 之中。这个问题对于绘制 饼图 及处理手势事件校验非常重要。

Flutter 绘制探索 | 扇形区域与点击校验_顺时针_02


1. 扇形区域的定义

首先来明确一下扇形区域的表示,如下图所示,一个 扇形区域 通过五个属性进行描述:

属性名

类型

作用

center

Offset

扇心

innerRadius

double

小圆半径

outRadius

double

大圆半径

startAngle

double

起始角度: 与横纵夹角(弧度)

sweepAngle

double

扫描角度: 弧度值,顺时针为正

这里通过 SectorShape 类对扇区的属性进行维护, 定义如下:

Flutter 绘制探索 | 扇形区域与点击校验_android_03

class SectorShape {
  Offset center; // 中心点
  double innerRadius; // 小圆半径
  double outRadius; // 大圆半径
  double startAngle; // 起始弧度
  double sweepAngle; // 扫描弧度

  SectorShape({
    required this.center,
    required this.innerRadius,
    required this.outRadius,
    required this.startAngle,
    required this.sweepAngle,
  });
}

2. 绘制扇形区域

接下来看一下如何绘制扇形区域,思路是先生成 区域路径 ,然后绘制路径。在生成路径的过程中,需要知道四个端点的坐标,如下所示:

Flutter 绘制探索 | 扇形区域与点击校验_架构师_04

根据 SectorShape 的属性,可以很轻松地计算出四点的坐标,如下所示:其中 shapeSectorShape 类型对象:

double startRad = shape.startAngle;
double endRad = shape.startAngle + shape.sweepAngle;
double r0 = shape.innerRadius;
double r1 = shape.outRadius;

Offset p0 = Offset(cos(startRad) * r0, sin(startRad) * r0);
Offset p1 = Offset(cos(startRad) * r1, sin(startRad) * r1);
Offset q0 = Offset(cos(endRad) * r0, sin(endRad) * r0);
Offset q1 = Offset(cos(endRad) * r1, sin(endRad) * r1);

下面是通过四点形成扇形区域路径的过程,其中 arcToPoint 能做出指定终点的圆弧路径,详细介绍可免费参见 : 《妙笔生花-第五章》 相关方法。

Path path = Path()
..moveTo(p0.dx, p0.dy)
..lineTo(p1.dx, p1.dy)
..arcToPoint(q1, radius: Radius.circular(r1), clockwise: false)
..lineTo(q0.dx, q0.dy)
..arcToPoint(p0, radius: Radius.circular(r0));

由于 SectorShape 的属性能唯一对应一种扇形区域。 使用为了使用方便,可以在 SectorShape 中提供一个 formPath 来生成路径:另外要注意,需要根据 sweepAngle 的正负确定顺时针与否;根据 sweepAngle 是否大于 pi ,确定区分取大弧。最后,根据 center 坐标对路径进行平移操作。

---->[SectorShape#formPath]----
Path formPath() {
  double startRad = startAngle;
  double endRad = startAngle + sweepAngle;

  double r0 = innerRadius;
  double r1 = outRadius;
  Offset p0 = Offset(cos(startRad) * r0, sin(startRad) * r0);
  Offset p1 = Offset(cos(startRad) * r1, sin(startRad) * r1);
  Offset q0 = Offset(cos(endRad) * r0, sin(endRad) * r0);
  Offset q1 = Offset(cos(endRad) * r1, sin(endRad) * r1);

  bool large = sweepAngle.abs() > pi;
  bool clockwise = sweepAngle > 0;

 Path path = Path()
    ..moveTo(p0.dx, p0.dy)
    ..lineTo(p1.dx, p1.dy)
    ..arcToPoint(q1, radius: Radius.circular(r1), clockwise: clockwise, largeArc: large)
    ..lineTo(q0.dx, q0.dy)
    ..arcToPoint(p0, radius: Radius.circular(r0), clockwise: !clockwise, largeArc: large);
  return path.shift(center);
}

这样就可以很轻松的通过 SectorShape 对象,来展示一个扇形区域。其中你可以通过操作 Paint 画笔,来实现更多的效果:比如使用的 shader 在扇形区域内填充图片、渐变等,这些基础可参见小册。

填充颜色

填充图像1

填充图像1

Flutter 绘制探索 | 扇形区域与点击校验_ci_05

Flutter 绘制探索 | 扇形区域与点击校验_android_06

Flutter 绘制探索 | 扇形区域与点击校验_flutter_07

Paint paint = Paint()
  ..style = PaintingStyle.stroke
  ..color = Colors.blue
  ..strokeWidth = 2;

SectorShape shape = SectorShape(
  center: Offset(size.width / 2, size.height / 2),
  innerRadius: 40,
  outRadius: 80,
  startAngle: 30 * pi / 180,
  sweepAngle: -80 * pi / 180,
);

canvas.drawPath(shape.formPath(), paint);

3. 扇形区域的点击校验

下面来思考一个问题:当手指或鼠标点在界面上,如何校验该点是否在 扇形区域 之内。如下图,很明显 p1 在其中,p2 不在。如何通过代码进行校验呢?

Flutter 绘制探索 | 扇形区域与点击校验_android_08

其实,思路很简单,点落在扇心区域内,需要满足两个条件:

[1]. 扇心与落点的距离 d 在 [innerRadius,outRadius]。
[2]. 扇心与落点的夹角在 [startAngle,startAngle+sweepAngle] 之间。

由于扇形区域的信息都存储在 SectorShape 类中,所以可以在其中定义 contains 方法,用于校验点是否在扇形区内:

---->[SectorShape#contains]----
bool contains(Offset p) {
  // Todo
}

下面,来实现如下效果,在按下时,落点在扇心区域内时,区域显示填充色示意,抬起时恢复:

Flutter 绘制探索 | 扇形区域与点击校验_android_09


校验逻辑如下,其中 校验环形区域 非常简单,落点与中心距离算出来比较一下即可。如果不再环中,就可以立刻判定为失败并返回。关于角度的校验:逆时针扫描时,终点小于起点;顺时针扫描时,终点大于起点;所以要根据 sweepAngle 区分判断:

---->[SectorShape#contains]----
bool contains(Offset p) {
  // 校验环形区域
  double l = (p - center).distance;
  bool inRing = l <= outRadius && l >= innerRadius;
  if (!inRing) return false;

  // 校验角度范围
  double a = (p - center).direction;
  double endArg = startAngle + sweepAngle;
  double start = startAngle;
  if(sweepAngle > 0){
    return a >= start && a <= endArg;
  }else{
    return a <= start && a >= endArg;
  }
}

这样在绘制时,只要通过下面 tag1 处代码,使用 shape.contains 方法,就能校验 p 点是否在扇形区内,如果在,则绘制扇形填充。核心的逻辑就是这些,想看详细的效果可参见源码: 【sector/02】

---->[Paper#paint]----
SectorShape shape = SectorShape(
  center: Offset(size.width / 2, size.height / 2),
  innerRadius: 40,
  outRadius: 80,
  startAngle: 30 * pi / 180,
  sweepAngle: 280 * pi / 180,
);

bool contain = shape.contains(p); // tag1

if(contain){
 canvas.drawPath(shape.formPath(), paint);
 Paint paint2 = Paint()..style=PaintingStyle.stroke;
 canvas.drawPath(shape.formPath(), paint2);
}

4. 几何校验与 Path 校验的区别

有些聪明的小伙伴可能会问:

Flutter 绘制探索 | 扇形区域与点击校验_顺时针_10

能问出这个问题,说明对绘制的基础掌握的还是比较牢固的。Path#contains 方法对于不规则的图形校验是至上法宝。但对于标准图形,通过几何方法进行校验比较简单,就像到楼下超市买瓶饮料,没必要开车去买。


为此,我做了一个小测试,看看两者在 百万次校验下的表现。如下 tag1 是使用 Path 判断,tag2 是上面基于几何型的判断。进行了两组四项测试,表现如下:

百万次耗时

Path 校验

几何形校验

点不在区域

230 ms

35 ms

点在区域

480 ms

110 ms

SectorShape shape = SectorShape(
  center: Offset.zero,
  innerRadius: 40,
  outRadius: 80,
  startAngle: 30 * pi / 180,
  sweepAngle: 80 * pi / 180,
);

// Offset offset = const Offset(112.7, 148.4);
Offset offset = const Offset(0, 0);
Path path = shape.formPath();
int time = DateTime.now().millisecondsSinceEpoch;
for (int i = 0; i < 1000000; i++) {
  // path.contains(offset); // tag1
  // shape.contains(offset); // tag2
}
int endTime = DateTime.now().millisecondsSinceEpoch;
print('${endTime - time}ms');

可以看出通过几何形的判断要快一些,这也是最直接的方式。当初步校验不合格,可以直接结束判断,而且其中只是基本的运算符计算,没有涉及复杂的循环判断。对于标准图形来说,这种方式既有效,又便捷,是比较好的。

Flutter 绘制探索 | 扇形区域与点击校验_flutter_11

但千万别会错意,我并不是说 path.contains 方法耗时,百万次才耗时两三百毫秒,如果不是超大批量的路径遍历校验,基本上也没什么影响。一般界面上同时校验几十个路径就顶天了,所以也不用担心,就像一秒赚几百万,不必要为丢了一毛钱而忧心忡忡。


到这里,扇形区域路径的获取、绘制与点击校验就完成了。对于 饼状图 而言,相当于最基础的材料已经准备完毕。下一篇,将基于本文的扇形区域,简单实现一个 饼状统计图 。那本文就到这里,谢谢观看 ~

标签:double,校验,shape,扇形,Offset,Flutter,SectorShape
From: https://blog.51cto.com/u_16163442/6512290

相关文章

  • 快速入门|Flutter完整开发实战详解 谷歌架构师独家分享
    前言这几年在大前端的开发领域,选择跨端方案的公司和部门越来越多,一方面是跨平台的前端框架越来越成熟,另一方面也是因原生开发者正逐年减少。所以,在当下掌握一门跨平台的技术栈还是很有必要的,无论从广度还是从深度都会有所帮助。就目前来说有很多主流的跨平台框架,就比如:Flutter、Rea......
  • 自定义异常和统一校验参数
    自定义异常@GetterpublicclassBusinessExceptionextendsRuntimeException{/***http状态码*/privateintcode;privateObjectobject;publicBusinessException(Stringmessage,intcode,Objectobject){super(message);......
  • flutter系列之:做一个图像滤镜
    目录简介我们的目标带滤镜的图片打造filter按钮打造可滑动按钮最后要解决的问题简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起来看看吧。......
  • 循环码的编码、译码与循环冗余校验
    本专栏包含信息论与编码的核心知识,按知识点组织,可作为教学或学习的参考。markdown版本已归档至【Github仓库:<https://github.com/timerring/information-theory>】或者【AIShareLab】回复信息论获取。循环码的编码循环码编码用硬件实现时,可用除法电路来实现。除法电路主要是......
  • Flutter 库:提升开发体验——Quick
    Flutter库:提升开发体验——Quick文章目录Flutter库:提升开发体验——Quick一、概述1、简介2、功能3、官方资料4、思考二、基本使用1、安装2、基本使用3、运行结果三、List列表扩展示例四、Map映射扩展示例五、其它示例一、概述1、简介Quick是一个功能强大的Flutter包,旨在通......
  • Flutter 使用 Key 强制重新渲染小部件
    Flutter使用Key强制重新渲染小部件文章目录Flutter使用Key强制重新渲染小部件一、Key的作用二、强制重新渲染小部件的步骤1、创建一个Key2、将Key分配给小部件3、强制重新渲染小部件三、代码案例一、Key的作用Key在Flutter中是一个抽象类,它有两个主要的子类:LocalK......
  • 身份证校验码:计算方法
    来源: 身份证校验码:计算方法 ......
  • flutter系列之:做一个图像滤镜
    简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起来看看吧。我们的目标在继续进行之前,我们先来讨论下本章到底要做什么。最终的目标是希......
  • flutter系列之:做一个图像滤镜
    目录简介我们的目标带滤镜的图片打造filter按钮打造可滑动按钮最后要解决的问题简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起来看看......
  • 实现一个权限校验注解
    什么是注解?Java注解是附加在代码中的一些元信息,用于编译和运行时进行解析和使用,起到说明、配置的功能。注解不会影响代码的实际逻辑,仅仅起到辅助性的作用。包含在java.lang.annotation包中。注解的定义类似于接口的定义,使用@interface来定义,定义一个方法即为注解类型定义了一个元......