转载烦请附加链接,以尊重本人开发成果。
博客编辑时间有限,简单粗暴。
只需要三个文件总共大概500行左右。
另外需要自备4张触点图片。
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'CorpImageWidget.dart';
// ignore: must_be_immutable
class DashLineWidget extends StatefulWidget{
final Key key;
Paint painter;
final Point p1,p2;
DashLineWidget(this.key,this.p1,this.p2,this.painter):super(key:key);
@override
State<DashLineWidget> createState() => DashLineState(this.p1,this.p2,this.painter);
}
class DashLineState extends State<DashLineWidget>{
Paint painter;
final Point p1,p2;
DashLineState(this.p1,this.p2,this.painter);
final double dashWidth = 5;
final double gapWidth = 3;
double startGap = 0;
Timer _timer;
void _startTimer(){
if(_timer!=null) return;
_timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
setState(() {
startGap+=0.5;
startGap%=(dashWidth+gapWidth);
});
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
_startTimer();
return CustomPaint(painter: DashLinePainter(p1.offset(),p2.offset(),painter,startGap,dashWidth,gapWidth),);
}
}
/// 线段画笔
class DashLinePainter extends CustomPainter {
//double和0比较的精度差,如果在这个精度里,那么这个值就当做0
final double accuracy = 0.000001;
//实线长度
final double dashLineLength;
//起始绘制长度[0,dashLineLength+gapLineLength] 如果超过虚线那么剩下的长度用来绘制起始实线
final double startGapLength;
//虚线长度
final double gapLineLength;
//起始绘制点
final Offset p1;
//终止绘制点
final Offset p2;
//y = kx + b 由p1和p2确定k、b的值
double k;
double b;
final Paint painter;
DashLinePainter(this.p1,this.p2,this.painter,[this.startGapLength = 0,this.dashLineLength = 5,this.gapLineLength=2]){
double y1 = p1.dy, y2 = p2.dy;
double x1 = p1.dx, x2 = p2.dx;
this.k = (y1-y2) / (x1-x2);
this.b = (y1*x2 - y2*x1)/(x2 - x1);
}
void drawLine(Canvas canvas, Offset p1,Offset p2){
// print("drawline: $p1 $p2");
canvas.drawLine(p1, p2, painter);
}
@override
void paint(Canvas canvas, Size size) {
if(k.abs()<accuracy){
drawHrizontalLine(canvas);
return;
}
if(k.isNaN||k.isInfinite){
drawVerticalLine(canvas);
return;
}
drawNormalLine(canvas);
}
void drawNormalLine(Canvas canvas){
print("DashLinePainter -> drawNormalLine $k $p1 $p2");
double radians = atan(k);
double deataY = sin(radians);
double deataX = cos(radians);
bool isDecreaseX = p1.dx>p2.dx;
bool isDecreaseY = p1.dy>p2.dy;
if(k<0&&!isDecreaseY || k>0&&isDecreaseY){
deataY = -deataY;
}
if(isDecreaseX) deataX = -deataX;
double gapX = gapLineLength * deataX, gapY = gapLineLength * deataY;
double dashX = dashLineLength * deataX, dashY = dashLineLength * deataY;
double x = p1.dx;
double y = p1.dy;
if(startGapLength>gapLineLength){
x += (startGapLength - gapLineLength) * deataX;
y += (startGapLength - gapLineLength) * deataY;
drawLine(canvas, Offset(p1.dx,p1.dy),Offset(isDecreaseX?max(x,p2.dx):min(x,p2.dx),isDecreaseY?max(y,p2.dy):min(y,p2.dy)));
x += gapLineLength * deataX;
y += gapLineLength * deataY;
}
else{
x += startGapLength*deataX;
y += startGapLength*deataY;
}
while(((!isDecreaseX)&&(x<=p2.dx)) || (isDecreaseX&&(x>=p2.dx)) ){
if((!isDecreaseX)&&(x + dashX < p2.dx) || isDecreaseX&&(x+dashX>p2.dx)){
drawLine(canvas, Offset(x,y),Offset(x+dashX,y+dashY));
}
else{
drawLine(canvas, Offset(x,y),Offset(p2.dx,p2.dy),);
}
x += dashX;
y += dashY;
x += gapX;
y += gapY;
}
}
void drawHrizontalLine(Canvas canvas){
print("DashLinePainter -> drawHrizontalLine $p1 $p2");
double x = p1.dx;
bool isDecrease = p1.dx>p2.dx;
double startGapX = isDecrease?-startGapLength:startGapLength;
double gapX = isDecrease?-gapLineLength:gapLineLength;
double dashX = isDecrease?-dashLineLength:dashLineLength;
if(startGapLength>gapLineLength){
x += startGapX - gapX;
drawLine(canvas, Offset(p1.dx,p1.dy),Offset(isDecrease?max(p2.dx,x):min(p2.dx,x),p1.dy),);
x += gapX;
}
else{
x += startGapX;
}
while(!isDecrease&&x<=p2.dx|| isDecrease&&x>=p2.dx){
drawLine(canvas,Offset(x,p1.dy),Offset(isDecrease?max(x+dashX,p2.dx):min(x+dashX,p2.dx),p1.dy),);
x+=dashX;
x+=gapX;
}
}
void drawVerticalLine(Canvas canvas){
print("DashLinePainter -> drawVerticalLine $p1 $p2");
double y = p1.dy;
bool isDecrease = p1.dy>p2.dy;
double startGapY = isDecrease?-startGapLength:startGapLength;
double gapY = isDecrease?-gapLineLength:gapLineLength;
double dashY = isDecrease?-dashLineLength:dashLineLength;
if(startGapLength>gapLineLength){
y += startGapY - gapY;
drawLine(canvas, Offset(p1.dx,p1.dy),Offset(p1.dx,isDecrease?max(p2.dy,y):min(p2.dy,y)),);
y += gapY;
}
else{
y += startGapY;
}
while(!isDecrease&&y<=p2.dy || isDecrease&&y>=p2.dy){
drawLine(canvas, Offset(p1.dx,y),Offset(p1.dx,isDecrease?max(p2.dy,y+dashY):min(p2.dy,y+dashY)),);
y += dashY;
y += gapY;
}
}
@override
bool shouldRepaint(DashLinePainter oldDelegate) => true;
}
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'CorpImageWidget.dart';
class CornerImageWidget extends StatefulWidget{
final Key key;
final String image;
final Point p;
final Paint painter;
CornerImageWidget(this.key,this.image,this.painter,this.p):super(key:key);
@override
State<CornerImageWidget> createState(){
return CornerImageState(this.image,this.painter,this.p);
}
}
class CornerImageState extends State<CornerImageWidget>{
final String image;
final Point p;
final Paint painter;
ui.Image imgData;
Future<ui.Image> load(String asset) async {
ByteData data = await rootBundle.load(asset);
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
ui.FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
@override
void initState() {
super.initState();
print("CornerImageWidget $image");
load(image).then((value){setState(() {
imgData = value;
});});
}
CornerImageState(this.image,this.painter,this.p);
@override
Widget build(BuildContext context) {
return CustomPaint(painter: CornerImagePainter(imgData,painter,p.offset()),);
}
}
class CornerImagePainter extends CustomPainter {
final ui.Image image;
final Paint painter;
final Offset p;
CornerImagePainter(this.image,this.painter,this.p);
@override
void paint(Canvas canvas, Size size) {
if(image!=null){
canvas.drawImageRect(image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
Rect.fromLTWH(p.dx-10, p.dy-10, 20, 20),
painter);
}
}
@override
bool shouldRepaint(CornerImagePainter oldDelegate) {
return oldDelegate.p!=p||oldDelegate.image!=image;
}
}
import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'CornerImageWidget.dart';
import 'DashLineWidget.dart';
class Point{
double x,y;
Point([this.x = 0,this.y = 0]);
Offset offset()=>Offset(x,y);
void update(Offset offset){
x = offset.dx;
y = offset.dy;
}
@override
String toString() => "($x,$y)";
}
class CropImageWidget extends StatefulWidget{
@override
State<CropImageWidget> createState() => _CropImageState();
}
class _CropImageState extends State<CropImageWidget>{
static const Offset _TL = Offset(100,100);
static const Offset _TR = Offset(200,100);
static const Offset _BL = Offset(100,200);
static const Offset _BR = Offset(200,200);
Paint painter;
//静止状态下的offset
Offset idleOffset=Offset(0, 0);
//本次移动的offset
Offset moveOffset=Offset(0, 0);
//最后一次down事件的offset
Offset lastStartOffset=Offset(0, 0);
final Point pTL = Point();
final Point pTR = Point();
final Point pBL = Point();
final Point pBR = Point();
///当前widget所绘制虚线的状态 用于更新移动时所相关的线
final GlobalKey<DashLineState> kT = GlobalKey();//top
final GlobalKey<DashLineState> kR = GlobalKey();//right
final GlobalKey<DashLineState> kB = GlobalKey();//bottom
final GlobalKey<DashLineState> kL = GlobalKey();//left
///当前widget所绘制移动触点的状态 用于更新移动点
final GlobalKey<CornerImageState> kTL = GlobalKey();//top left
final GlobalKey<CornerImageState> kTR = GlobalKey();//top right
final GlobalKey<CornerImageState> kBL = GlobalKey();//bottom left
final GlobalKey<CornerImageState> kBR = GlobalKey();//bottom right
//region 选择移动点
Point p;
List<GlobalKey> g;
List<Point> lstPoints;
List<List<GlobalKey>> lstStates;
ui.Image imgTL,imgTR,imgBL,imgBR;
void selectPoint(Offset offset){
// print(" selectPoint 1");
if(isSelectedPoint()) return;
double x = offset.dx;
double y = offset.dy;
for(int i=0;i<4;++i){
Point _p = lstPoints[i];
double x1 = _p.x - 10;
double x2 = _p.x + 10;
double y1 = _p.y - 10;
double y2 = _p.y + 10;
if(x>=x1&&x<=x2&&y>=y1&&y<=y2){
p = _p;
g = lstStates[i];
lastStartOffset = offset;
idleOffset = _p.offset();
// print(" selectPoint 2");
break;
}
}
}
bool isSelectedPoint() => p!=null;
void updatePoint(Offset offset){
// print(" updatePoint 1");
if(isSelectedPoint()){
moveOffset=offset-lastStartOffset+idleOffset;
moveOffset=Offset(max(0, moveOffset.dx), max(0, moveOffset.dy));
p.update(moveOffset);
for(GlobalKey k in g){
k.currentState.setState(() {});
}
// print(" updatePoint 2 $p ");
// print(" updatePoint 3 $pTL, $pTR, $pBL, $pBR ");
}
}
void releasePoint(){
// print(" releasePoint");
p = null;
}
//endregion
// ignore: non_constant_identifier_names
_CropImageState([Offset TL = _TL,Offset TR = _TR, Offset BL = _BL, Offset BR = _BR]){
pTL.update(TL);
pTR.update(TR);
pBL.update(BL);
pBR.update(BR);
lstPoints = [pTL, pTR, pBL, pBR];
lstStates = [
[kTL,kT,kL],
[kTR,kT,kR],
[kBL,kB,kL],
[kBR,kB,kR],
];
}
@override
void initState(){
painter = Paint()
..color = Color(0xffef6c00)
..style = PaintingStyle.fill
..isAntiAlias = true
..filterQuality = FilterQuality.high
..strokeWidth = 1.5;
super.initState();
}
@override
void deactivate() {
super.deactivate();
releasePoint();
}
@override
Widget build(BuildContext context) {
return Material(
child: GestureDetector(
onPanStart: (detail) {
// print("onPanStart ${detail.globalPosition}");
selectPoint(detail.globalPosition);
},
onPanUpdate: (detail){
// print("onPanUpdate ${detail.globalPosition}");
updatePoint(detail.globalPosition);
},
onPanEnd: (detail) {
// print("onPanEnd");
releasePoint();
},
child: Ink(
child: Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: DashLineWidget(kT,pTL,pTR,painter),
),
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: DashLineWidget(kR,pTR,pBR,painter),
),
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: DashLineWidget(kB,pBR,pBL,painter),
),
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: DashLineWidget(kL,pBL,pTL,painter),
),
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: CornerImageWidget(kTL,'assets/images/ic_fixation_tl.png', painter, pTL),
),
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: CornerImageWidget(kTR,'assets/images/ic_fixation_tr.png', painter, pTR),
),
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: CornerImageWidget(kBL,'assets/images/ic_fixation_bl.png', painter, pBL),
),
Positioned(left: 0,right: 0,top: 0,bottom: 0,
child: CornerImageWidget(kBR,'assets/images/ic_fixation_br.png', painter, pBR)
),
],
),
),
)
);
}
}
包含整个小项目的全部代码。
当然整个项目只需要以上提及的三个dart文件,以及展示的图片,无需下载也可直接使用,下载仅是直观使用或者便捷复制。
CSDN下载:https://download.csdn.net/download/best335/14092462
代码冗余度还有很大的优化空间。
启发:https://blog.csdn.net/PrinceBB/article/details/108133286
控件拖动:https://blog.csdn.net/act64/article/details/89889266
画笔使用:https://blog.csdn.net/u011272795/article/details/83828732
assets图片加载成ui.Image:https://blog.csdn.net/qq_34476727/article/details/107519603