初始化
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
class PointItem {
|
||||
double x;
|
||||
double y;
|
||||
int? index;
|
||||
bool isSelected;
|
||||
bool isError;
|
||||
bool isFirstSelected;
|
||||
double angle;
|
||||
|
||||
PointItem({
|
||||
required this.x,
|
||||
required this.y,
|
||||
this.index,
|
||||
this.isSelected = false,
|
||||
this.isError = false,
|
||||
this.isFirstSelected = false,
|
||||
this.angle = double.infinity,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is PointItem && runtimeType == other.runtimeType && x == other.x && y == other.y;
|
||||
|
||||
@override
|
||||
int get hashCode => x.hashCode ^ y.hashCode;
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../../../tools_base/debug_log.dart';
|
||||
import '../model/point_item.dart';
|
||||
import 'line_painter.dart';
|
||||
|
||||
/// 当点被选中时的回调函数
|
||||
typedef OnHitPoint = void Function(List<int> result);
|
||||
|
||||
/// 手势滑动结束时的回调函数
|
||||
/// [result] 已经选择的所有点的结果集
|
||||
typedef OnComplete = void Function(List<int> result);
|
||||
|
||||
class GesturePasswordWidget extends StatefulWidget with DiagnosticableTreeMixin {
|
||||
/// GesturePasswordWidget 的 width 和 height.
|
||||
final double size;
|
||||
|
||||
///用来判断点是否被选中的区域大小,值越大识别越精准.
|
||||
final double identifySize;
|
||||
|
||||
///正常情况下展示的widget
|
||||
final Widget? normalItem;
|
||||
|
||||
///选中情况下展示的widget
|
||||
final Widget? selectedItem;
|
||||
|
||||
/// 错误情况下展示的widget,只有设置了[minLength]或[answer]时才会生效,
|
||||
/// 1)当[minLength]不为null时,如果选择的点的数量小于minLength,则展示[errorItem],
|
||||
/// 比如设置了 minLength = 4,但是选择的点的结果集为 [0,1,3],共选择了3个点,小于4;
|
||||
/// 2)当[answer]不为null时,如果选择的点的结果集和[answer]不相等,则展示[errorItem],
|
||||
/// 比如 answer = [0,1,2,4,7],但是选择的点的结果集为[0,1,2,5,8],和answer不相等;
|
||||
/// 另外,[errorItem]的展示时长由[completeWaitMilliseconds]控制。
|
||||
final Widget? errorItem;
|
||||
|
||||
/// 当这个点被选中时要展示的widget,其展示时长由[hitShowMilliseconds]控制,达到展示时长
|
||||
/// 后继续展示[selectedItem]。
|
||||
final Widget? hitItem;
|
||||
|
||||
///正常情况下显示的箭头控件。
|
||||
///跟随手势旋转时,x轴正方向为0度,所以如果你使用了箭头,确保箭头指向x轴正方向。
|
||||
final Widget? arrowItem;
|
||||
|
||||
///错误情况下显示的箭头控件,如果设置了[errorArrowItem],则必须设置[arrowItem],
|
||||
///否则[errorArrowItem]不会展示。
|
||||
///跟随手势旋转时,x轴正方向为0度,所以如果你使用了箭头,确保箭头指向x轴正方向。
|
||||
final Widget? errorArrowItem;
|
||||
|
||||
///[arrowItem]和[errorArrowItem]在x轴上的偏移,原点在[normalItem]的中心。
|
||||
///当 -1 < [arrowXAlign] < 1 时,[arrowItem]和[errorArrowItem]在[normalItem]范围内进行绘制;
|
||||
///当[arrowXAlign] > 1 或者[arrowXAlign] < -1时,在[normalItem]范围外进行绘制;
|
||||
final double arrowXAlign;
|
||||
|
||||
///[arrowItem]和[errorArrowItem]在y轴上的偏移,原点在[normalItem]的中心。
|
||||
///当 -1 < [arrowYAlign] < 1 时,[arrowItem]和[errorArrowItem]在[normalItem]范围内进行绘制;
|
||||
///当[arrowYAlign] > 1 或者[arrowYAlign] < -1时,在[normalItem]范围外进行绘制;
|
||||
final double arrowYAlign;
|
||||
|
||||
///单行个数,总个数等于 singleLineCount * singleLineCount.
|
||||
final int singleLineCount;
|
||||
|
||||
///GesturePasswordWidget的背景色,默认为 [Theme.of].[scaffoldBackgroundColor]
|
||||
final Color? color;
|
||||
|
||||
///当点被选中时的回调函数
|
||||
final OnHitPoint? onHitPoint;
|
||||
|
||||
///手势滑动结束时的回调函数
|
||||
final OnComplete? onComplete;
|
||||
|
||||
///线的颜色
|
||||
final Color lineColor;
|
||||
|
||||
///错误场景下线的颜色,见[errorItem]
|
||||
final Color errorLineColor;
|
||||
|
||||
///线的宽度
|
||||
final double lineWidth;
|
||||
|
||||
/// 是否采用宽松策略,默认为true。
|
||||
/// 考虑这种情况:第一个点选中了 index = 0 的点,第二个点选中了 index = 6的点,
|
||||
/// 此时index = 0,index = 3,index = 6这三个点在一条直线上,
|
||||
/// 如果loose为true,输出为[0,3,6],
|
||||
/// 如果loose为false,输出为[0,6].
|
||||
final bool loose;
|
||||
|
||||
///正确的结果,demo: [0, 1, 2, 4, 7]
|
||||
final List<int>? answer;
|
||||
|
||||
///最后选择的所有点及绘制的直线在屏幕上展示的时间,时间结束后,清除所有点,恢复到初始状态,
|
||||
///时间结束之前 GesturePasswordWidget 不再接受任何手势事件。
|
||||
final int completeWaitMilliseconds;
|
||||
|
||||
/// 只是用来展示用,不能触摸
|
||||
final bool ignoring;
|
||||
|
||||
///见[hitItem]
|
||||
final int hitShowMilliseconds;
|
||||
|
||||
///如果设置了此值,则长度不够时显示[errorItem]和[errorLineColor].
|
||||
final int? minLength;
|
||||
|
||||
///是否开启触觉反馈(命中点轻震、绘制错误较重震),默认开启
|
||||
final bool enableHaptic;
|
||||
|
||||
GesturePasswordWidget({
|
||||
super.key,
|
||||
this.size = 300.0,
|
||||
this.identifySize = 50.0,
|
||||
this.normalItem,
|
||||
this.selectedItem,
|
||||
this.errorItem,
|
||||
this.hitItem,
|
||||
this.arrowItem,
|
||||
this.errorArrowItem,
|
||||
this.arrowXAlign = 0.6,
|
||||
this.arrowYAlign = 0.0,
|
||||
this.singleLineCount = 3,
|
||||
this.color,
|
||||
this.ignoring = false,
|
||||
this.onHitPoint,
|
||||
this.onComplete,
|
||||
this.lineColor = Colors.green,
|
||||
this.errorLineColor = Colors.redAccent,
|
||||
this.lineWidth = 2.0,
|
||||
this.answer,
|
||||
this.loose = true,
|
||||
this.completeWaitMilliseconds = 300,
|
||||
this.hitShowMilliseconds = 40,
|
||||
this.minLength,
|
||||
this.enableHaptic = true,
|
||||
}) : assert(singleLineCount > 1, 'singLineCount must not be smaller than 1'),
|
||||
assert(identifySize > 0),
|
||||
assert(size > identifySize),
|
||||
assert(!(errorArrowItem != null && arrowItem == null), 'when arrowItem == null, errorArrowItem will not be shown.');
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty('size', size));
|
||||
properties.add(DoubleProperty('identifySize', identifySize));
|
||||
properties.add(DiagnosticsProperty<Widget>('normalItem', normalItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('selectedItem', selectedItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('errorItem', errorItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('hitItem', hitItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('arrowItem', arrowItem));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Widget>('errorArrowItem', errorArrowItem),
|
||||
);
|
||||
properties.add(DoubleProperty('arrowXAlign', arrowXAlign));
|
||||
properties.add(DoubleProperty('arrowYAlign', arrowYAlign));
|
||||
properties.add(IntProperty('singleLineCount', singleLineCount));
|
||||
properties.add(ColorProperty('color', color));
|
||||
properties.add(DiagnosticsProperty<OnHitPoint>('onHitPoint', onHitPoint));
|
||||
properties.add(DiagnosticsProperty<OnComplete>('onComplete', onComplete));
|
||||
properties.add(ColorProperty('lineColor', lineColor));
|
||||
properties.add(ColorProperty('errorLineColor', errorLineColor));
|
||||
properties.add(IterableProperty('answer', answer));
|
||||
properties.add(DoubleProperty('lineWidth', lineWidth));
|
||||
properties.add(FlagProperty(
|
||||
'loose',
|
||||
value: loose,
|
||||
ifFalse: 'loose: false',
|
||||
ifTrue: 'loose: true',
|
||||
defaultValue: true,
|
||||
));
|
||||
properties.add(
|
||||
IntProperty('completeWaitMilliseconds', completeWaitMilliseconds),
|
||||
);
|
||||
properties.add(IntProperty('hitShowMilliseconds', hitShowMilliseconds));
|
||||
properties.add(IntProperty('minLength', minLength));
|
||||
}
|
||||
|
||||
@override
|
||||
State<GesturePasswordWidget> createState() => _GesturePasswordWidgetState();
|
||||
}
|
||||
|
||||
class _GesturePasswordWidgetState extends State<GesturePasswordWidget> {
|
||||
late Point origin;
|
||||
late int totalCount;
|
||||
Point<double>? lastPoint;
|
||||
Widget? normalItem;
|
||||
Widget? defaultNormalItem;
|
||||
Widget? selectedItem;
|
||||
Widget? defaultSelectedItem;
|
||||
Widget? errorItem;
|
||||
Widget? defaultErrorItem;
|
||||
Color? lineColor;
|
||||
|
||||
String tipText = '绘制解锁图形';
|
||||
bool _completing = false; // 完成动画期间锁定手势,避免动画未结束就开始下一笔
|
||||
|
||||
final points = <PointItem>[];
|
||||
final linePoints = <Point<double>>[];
|
||||
final result = <int>[];
|
||||
final double defaultSize = 10.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
defaultNormalItem = Container(
|
||||
width: defaultSize,
|
||||
height: defaultSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.greenAccent,
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
);
|
||||
defaultSelectedItem = Container(
|
||||
width: defaultSize,
|
||||
height: defaultSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
);
|
||||
defaultErrorItem = Container(
|
||||
width: defaultSize,
|
||||
height: defaultSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.answer?.isNotEmpty ?? false) {
|
||||
tipText = '验证解锁图形';
|
||||
}
|
||||
|
||||
lineColor = widget.lineColor;
|
||||
normalItem = widget.normalItem ?? defaultNormalItem;
|
||||
selectedItem = widget.selectedItem ?? defaultSelectedItem;
|
||||
errorItem = widget.errorItem ?? defaultErrorItem;
|
||||
|
||||
totalCount = widget.singleLineCount * widget.singleLineCount;
|
||||
origin = Point<double>(widget.size * 0.5, widget.size * 0.5);
|
||||
calculatePointPosition();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(GesturePasswordWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.lineColor != oldWidget.lineColor) {
|
||||
lineColor = widget.lineColor;
|
||||
}
|
||||
|
||||
if (widget.normalItem != oldWidget.normalItem) {
|
||||
normalItem = widget.normalItem ?? defaultNormalItem;
|
||||
}
|
||||
|
||||
if (widget.selectedItem != oldWidget.selectedItem) {
|
||||
selectedItem = widget.selectedItem ?? defaultSelectedItem;
|
||||
}
|
||||
|
||||
if (widget.errorItem != oldWidget.errorItem) {
|
||||
errorItem = widget.errorItem ?? defaultErrorItem;
|
||||
}
|
||||
|
||||
if (widget.singleLineCount != oldWidget.singleLineCount ||
|
||||
widget.size != oldWidget.size ||
|
||||
widget.identifySize != oldWidget.identifySize) {
|
||||
totalCount = widget.singleLineCount * widget.singleLineCount;
|
||||
origin = Point<double>(widget.size * 0.5, widget.size * 0.5);
|
||||
points.clear();
|
||||
calculatePointPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//预览小点的边长:每行 singleLineCount 个 10px 的点 + 之间 8px 间距。必须按 singleLineCount 算,
|
||||
//写死值(原来是 56)比内容宽时 Wrap 默认贴左上,整块预览会偏左偏上,且换行数只对 3x3 成立
|
||||
final previewSize = 10.0 * widget.singleLineCount + 8 * (widget.singleLineCount - 1);
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: previewSize,
|
||||
height: previewSize,
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: points
|
||||
.map((e) => Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
//选中态靠加粗边框填实心,边框画在 10x10 内部,不影响尺寸和对齐
|
||||
border: Border.all(width: e.isSelected ? 5 : 1.0, color: const Color(0xff6792ff)),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text(
|
||||
tipText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: widget.ignoring || _completing,
|
||||
child: Container(
|
||||
color: widget.color ?? Theme.of(context).scaffoldBackgroundColor,
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: Stack(
|
||||
children: createPointsWidget()
|
||||
..add(
|
||||
GestureDetector(
|
||||
onPanDown: handlePanDown,
|
||||
onPanUpdate: handlePanUpdate,
|
||||
onPanEnd: handlePanEnd,
|
||||
onPanCancel: () {
|
||||
handlePanEnd(null);
|
||||
},
|
||||
child: CustomPaint(
|
||||
painter: LinePainter(
|
||||
points: linePoints,
|
||||
lineColor: lineColor,
|
||||
lineWidth: widget.lineWidth,
|
||||
),
|
||||
willChange: true,
|
||||
size: Size(widget.size, widget.size),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//计算每个点的位置
|
||||
void calculatePointPosition() {
|
||||
double initX = widget.identifySize * 0.5;
|
||||
double initY = widget.identifySize * 0.5;
|
||||
double gap = (widget.size - widget.identifySize) / (widget.singleLineCount - 1);
|
||||
|
||||
for (int i = 0; i < totalCount; i++) {
|
||||
double centerX = initX + i % widget.singleLineCount * gap;
|
||||
double centerY = initY + i ~/ widget.singleLineCount * gap;
|
||||
var point = PointItem(x: centerX, y: centerY, index: i);
|
||||
//展示模式(ignoring)下预置 answer 命中的点为选中
|
||||
if (widget.ignoring && (widget.answer?.contains(i) ?? false)) {
|
||||
point.isSelected = true;
|
||||
}
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
|
||||
//创建每个点的widget
|
||||
List<Widget> createPointsWidget() {
|
||||
return points.map<Widget>((p) {
|
||||
double reference = 1 - (widget.identifySize / widget.size);
|
||||
double x = (p.x - origin.x) / (widget.size * 0.5) / reference;
|
||||
double y = (p.y - origin.y) / (widget.size * 0.5) / reference;
|
||||
|
||||
Widget? child = normalItem;
|
||||
if (p.isError) {
|
||||
child = errorItem;
|
||||
} else if (p.isFirstSelected) {
|
||||
child = widget.hitItem;
|
||||
} else if (p.isSelected) {
|
||||
child = selectedItem;
|
||||
}
|
||||
|
||||
Widget? arrowItem = widget.arrowItem;
|
||||
if (p.isError && widget.errorArrowItem != null) {
|
||||
arrowItem = widget.errorArrowItem;
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment(x, y),
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
width: widget.identifySize,
|
||||
height: widget.identifySize,
|
||||
alignment: Alignment.center,
|
||||
child: widget.arrowItem == null || p.angle == double.infinity
|
||||
? child
|
||||
: Transform.rotate(
|
||||
angle: p.angle,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
child!,
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
widget.arrowXAlign,
|
||||
widget.arrowYAlign,
|
||||
),
|
||||
child: arrowItem,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void handlePanDown(DragDownDetails details) {
|
||||
Point<double> curPoint = Point(details.localPosition.dx, details.localPosition.dy);
|
||||
final point = calculateHitPoint(curPoint);
|
||||
if (point != null) {
|
||||
if (!linePoints.contains(Point(point.x, point.y))) {
|
||||
addPointToResult(point.index);
|
||||
setState(() {
|
||||
point.isSelected = true;
|
||||
linePoints.add(Point(point.x, point.y));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handlePanUpdate(DragUpdateDetails details) {
|
||||
Point<double> curPoint = Point(details.localPosition.dx, details.localPosition.dy);
|
||||
final hitPoint = calculateHitPoint(curPoint);
|
||||
if (hitPoint != null) {
|
||||
if (!linePoints.contains(Point(hitPoint.x, hitPoint.y))) {
|
||||
final drawPoint = Point(hitPoint.x, hitPoint.y);
|
||||
//宽松策略下,若三点共线则自动将中间的点设置为选中状态。
|
||||
if (widget.loose && linePoints.isNotEmpty) {
|
||||
handleLooseCase(points[result.last], hitPoint);
|
||||
}
|
||||
|
||||
//处理箭头的角度展示
|
||||
if (widget.arrowItem != null) {
|
||||
for (int i = 0; i < result.length - 1; i++) {
|
||||
final p1 = Point(points[result[i]].x, points[result[i]].y);
|
||||
final p2 = Point(points[result[i + 1]].x, points[result[i + 1]].y);
|
||||
points[result[i]].angle = calculateAngle(p1, p2);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
final p1 = Point(points[result.last].x, points[result.last].y);
|
||||
points[result.last].angle = calculateAngle(p1, Point(hitPoint.x, hitPoint.y));
|
||||
}
|
||||
addPointToResult(hitPoint.index);
|
||||
setState(() {
|
||||
linePoints.remove(lastPoint);
|
||||
hitPoint.isSelected = true;
|
||||
linePoints.add(drawPoint);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (linePoints.isNotEmpty) {
|
||||
if (widget.arrowItem != null) {
|
||||
final p1 = Point(points[result.last].x, points[result.last].y);
|
||||
points[result.last].angle = calculateAngle(p1, curPoint);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
linePoints.remove(lastPoint);
|
||||
linePoints.add(curPoint);
|
||||
});
|
||||
lastPoint = curPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handlePanEnd(DragEndDetails? details) async {
|
||||
if (result.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
linePoints.removeLast();
|
||||
|
||||
if ((widget.answer != null && widget.answer!.join() != result.join()) ||
|
||||
(widget.minLength != null && widget.minLength! > result.length)) {
|
||||
lineColor = widget.errorLineColor;
|
||||
if (widget.minLength != null && widget.minLength! > result.length) {
|
||||
tipText = '请至少链接4个点';
|
||||
} else {
|
||||
tipText = '图形绘制不正确';
|
||||
}
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
points[result[i]].isError = true;
|
||||
}
|
||||
if (widget.enableHaptic) HapticFeedback.mediumImpact(); //绘制错误:较重震动
|
||||
}
|
||||
|
||||
//清除最后一个点的角度
|
||||
points[result.last].angle = double.infinity;
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_completing = true;
|
||||
});
|
||||
await Future.delayed(Duration(
|
||||
milliseconds: widget.completeWaitMilliseconds,
|
||||
));
|
||||
_completing = false;
|
||||
lineColor = widget.lineColor;
|
||||
widget.onComplete?.call(result);
|
||||
debugLog('handlePanEnd answer=${widget.answer}, result = $result');
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
for (final p in points) {
|
||||
p.isSelected = false;
|
||||
p.isError = false;
|
||||
p.angle = double.infinity;
|
||||
}
|
||||
linePoints.clear();
|
||||
result.clear();
|
||||
});
|
||||
}
|
||||
|
||||
//计算命中的点
|
||||
PointItem? calculateHitPoint(Point<double> curPoint) {
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
final p = Point(points[i].x, points[i].y);
|
||||
if (p.distanceTo(curPoint) < widget.identifySize * 0.5) {
|
||||
if (points[i].isSelected) {
|
||||
return null;
|
||||
}
|
||||
return points[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void addPointToResult(int? index) {
|
||||
if (widget.enableHaptic) HapticFeedback.selectionClick(); //每命中一个点轻震,提升手感
|
||||
result.add(index ?? 0);
|
||||
widget.onHitPoint?.call(result);
|
||||
|
||||
if (widget.hitItem != null) {
|
||||
setState(() {
|
||||
points[index!].isFirstSelected = true;
|
||||
});
|
||||
Future.delayed(Duration(milliseconds: widget.hitShowMilliseconds), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
points[index!].isFirstSelected = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//根据海伦公式计算三角形面积,面积为0时视为三点共线。如果这个点还在共线的中间,
|
||||
//则将其设置为选中状态,并将其添加到result中。
|
||||
void handleLooseCase(PointItem pre, PointItem next) {
|
||||
List<int?> midItems = [];
|
||||
for (final item in points) {
|
||||
if (item != pre && item != next && item.isSelected == false) {
|
||||
final itemDrawPoint = Point<double>(item.x, item.y);
|
||||
final preDrawPoint = Point<double>(pre.x, pre.y);
|
||||
final nextDrawPoint = Point<double>(next.x, next.y);
|
||||
double a = itemDrawPoint.distanceTo(preDrawPoint);
|
||||
double b = itemDrawPoint.distanceTo(nextDrawPoint);
|
||||
double c = preDrawPoint.distanceTo(nextDrawPoint);
|
||||
double p = (a + b + c) * 0.5;
|
||||
double area = p * (p - a) * (p - b) * (p - c);
|
||||
|
||||
double halfDistance = c * 0.5;
|
||||
Point<double> mid = Point(
|
||||
(pre.x + next.x) * 0.5,
|
||||
(pre.y + next.y) * 0.5,
|
||||
);
|
||||
|
||||
if (area - 0.5 <= 0 && itemDrawPoint.distanceTo(mid) < halfDistance) {
|
||||
item.isSelected = true;
|
||||
midItems.add(item.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next.index! > pre.index!) {
|
||||
midItems.sort((a, b) => a! - b!);
|
||||
} else {
|
||||
midItems.sort((a, b) => b! - a!);
|
||||
}
|
||||
|
||||
for (final index in midItems) {
|
||||
addPointToResult(index);
|
||||
}
|
||||
}
|
||||
|
||||
//计算两点之间连线和 x 轴的夹角,返回弧度
|
||||
double calculateAngle(Point p1, Point p2) {
|
||||
return atan2((p2.y - p1.y), (p2.x - p1.x)); //弧度
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class LinePainter extends CustomPainter {
|
||||
final List<Point>? points;
|
||||
final Color? lineColor;
|
||||
final double? lineWidth;
|
||||
|
||||
LinePainter({
|
||||
this.points,
|
||||
this.lineColor,
|
||||
this.lineWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (points != null && points!.length > 1) {
|
||||
Paint paint = Paint();
|
||||
paint.strokeWidth = lineWidth!;
|
||||
paint.color = lineColor!;
|
||||
paint.isAntiAlias = true;
|
||||
paint.style = PaintingStyle.fill;
|
||||
paint.strokeCap = StrokeCap.round;
|
||||
|
||||
for (int i = 0; i < points!.length - 1; i++) {
|
||||
canvas.drawLine(
|
||||
Offset(points![i].x as double, points![i].y as double),
|
||||
Offset(points![i + 1].x as double, points![i + 1].y as double),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(LinePainter oldDelegate) => true;
|
||||
}
|
||||
Reference in New Issue
Block a user