颤音:缩放画布在图纸应用中缩放帆布后的补偿
因此,我正在使用Flutter制作绘图应用程序。我想实现捏合Zoom,同时仍然能够用一根手指在画布上绘画。我已经弄清楚了如何使用agipiatiatiatiatipiatiatiatiatiatipiatiatiatiatiatiatiatiatiatiatiatiationraggesturererecognizer
告诉屏幕上有多少手指,然后如果屏幕上有两个手指并在画布上绘画,则只有我自己的变焦功能一。一切与1的比例完美搭配。
我的问题是,在我放大并尝试再次绘画后,图纸被偏移到画布的左上方,并且我放大了越远。
我正在计算缩放偏移量,并在缩放期间拖动偏移量,然后将其传递到用transform
widget包装的画布背景(此部分工作正常)。然后,当屏幕上只有一根手指并且点将点发送到自定义画家时,我也经过了我传递给画布的比例尺和偏移,以使其具有相同的偏移和比例。
我已经找到了这种方法可以在没有2个手指检测的情况下工作,但是手势检测器也包裹在转换小部件中,它不在这里。
这是我使用缩放和油漆方法的手势检测器的代码:
GestureRecognizerFactoryWithHandlers<
ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(),
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = (Offset offset) {
//Touch Started
setState(() {
_counter++;
touchCallbacks.touchBegan(TouchData(_counter, offset));
});
if(touchCallbacks.taps.length == 2){
final RenderBox box = _boxKey.currentContext?.findRenderObject() as RenderBox;
boxOffset = box.localToGlobal(Offset.zero);
final boxsize = _boxKey.currentContext?.size;
boxLength = boxsize != null ? boxsize.width * _lastScale : 1;
boxHeight = boxsize != null ? boxsize.height * _lastScale : 1;
}
return ItemDrag((details, tId) {
//Touch Updated
if(touchCallbacks.taps.length == 2){
//zoom and drag function
double testScale = (((touchCallbacks.taps.first.offset - touchCallbacks.taps.last.offset).distance - touchCallbacks.startDistance)+ _lastScaleDistance)/40;
if(testScale>0 && testScale< 15) {
scaleDistance = ((touchCallbacks.taps.first.offset -
touchCallbacks.taps.last.offset).distance -
touchCallbacks.startDistance) +
_lastScaleDistance;
double pinchOriginX = touchCallbacks.firstTouch.dx;
double pinchOriginY = touchCallbacks.firstTouch.dy;
double transformOriginX = boxOffset.dx + boxLength / 2;
double transformOriginY = boxOffset.dy + boxHeight / 2;
double movement = scaleDistance - _lastScaleDistance;
// print(_lastScale);
double displacementX = (transformOriginX - pinchOriginX) / _lastScale;
double displacementY = (transformOriginY - pinchOriginY) / _lastScale;
//
correctedOffset = Offset(
_lastOffset.dx + ((displacementX * movement) / 40)
,
_lastOffset.dy + ((displacementY * movement) / 40)
);
_scale = scaleDistance / 40 + 1;
}
_dragOffset = Offset(
((((touchCallbacks.taps.first.offset.dx +
touchCallbacks.taps.last.offset.dx) / 2) -
touchCallbacks.firstTouch.dx)),
(((touchCallbacks.taps.first.offset.dy +
touchCallbacks.taps.last.offset.dy) / 2) -
touchCallbacks.firstTouch.dy));
finalOffset = _dragOffset + correctedOffset;
setState(() {
_transform = Matrix4(
_scale, 0, 0, 0, //
0, _scale, 0, 0, //
0, 0, 1, 0, //
finalOffset.dx, finalOffset.dy, 0, 1,
);
});
} else {
//paint method
if (firstTouch == false && touchCallbacks.taps.length < 2) {
// I used a bool to only call this only once every paint since i couldnt get the details for global position in the initial touch add part
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition) ;
final point = Point(offset.dx, offset.dy);
final points = [point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
setState(() {
firstTouch = true;
});
} else {
if (touchCallbacks.taps.length < 2) {
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition);
final point = Point(offset.dx, offset.dy);
final points = [...line.points, point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
}
}
}
setState(() {
touchCallbacks
.touchMoved(TouchData(tId, details.globalPosition));
});
}, (details, tId) {
//Touch Ended
if(touchCallbacks.taps.length == 2){
setState(() {
_lastOffset = finalOffset;
_lastScaleDistance = scaleDistance;
_lastScale = (scaleDistance/40) +1;
});
} else {
//add line to list of lines (drawing)
if(touchCallbacks.taps.length < 2) {
Stroke newline = Stroke(
line.points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset);
lines = List.from(lines)
..add(newline);
linesStreamController.add(lines);
setState(() {
firstTouch = false;
});
}
}
touchCallbacks
.touchEnded(TouchData(tId, const Offset(0, 0)));
}, (tId) {
touchCallbacks
.touchCanceled(TouchData(tId, const Offset(0, 0)));
}, _counter);
};
}),
这是我的custompainter
我使用的代码:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:perfect_freehand/perfect_freehand.dart';
import 'stroke.dart';
import 'stroke_options.dart';
class Sketcher2 extends CustomPainter {
final List<Stroke> lines;
final StrokeOptions options;
Sketcher2({required this.lines, required this.options});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = options.color;
for (int i = 0; i < lines.length; ++i) {
final outlinePoints = getStroke(
lines[i].points,
size: lines[i].size,
thinning: lines[i].thinning,
smoothing: lines[i].smoothing,
streamline: lines[i].streamline,
taperStart: lines[i].taperStart,
capStart: lines[i].capStart,
taperEnd: lines[i].taperEnd,
capEnd: lines[i].capEnd,
simulatePressure: lines[i].simulatePressure,
isComplete:lines[i].isComplete,
);
final path = Path();
Offset offset = Offset(-1*lines[i].offset.dx, -1*lines[i].offset.dy);
double scale = 1/lines[i].scale;
if (outlinePoints.isEmpty) {
return;
} else if (outlinePoints.length < 2) {
// If the path only has one line, draw a dot.
paint.color = lines[i].color;
path.addOval(Rect.fromCircle(
center: Offset((outlinePoints[0].x+offset.dx) * scale, (outlinePoints[0].y+offset.dy) * scale), radius: 1));
} else {
// Otherwise, draw a line that connects each point with a curve.
path.moveTo((outlinePoints[0].x+offset.dx) * scale, (outlinePoints[0].y+offset.dy) * scale);
for (int i = 1; i < outlinePoints.length - 1; ++i) {
final p0 = outlinePoints[i];
final p1 = outlinePoints[i + 1];
path.quadraticBezierTo(
(p0.x+offset.dx) * scale, (p0.y+offset.dy) * scale, ((p0.x+offset.dx) * scale + (p1.x+offset.dx) * scale) / 2, ((p0.y+offset.dy) * scale + (p1.y+offset.dy) * scale) / 2);
}
}
paint.color = lines[i].color;
canvas.drawPath(path, paint);
}
}
@override
bool shouldRepaint(Sketcher2 oldDelegate) {
return true;
}
}
这是我使用的完整代码:
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:perfect_freehand/perfect_freehand.dart';
import 'dart:ui';
import 'sketcher2.dart';
import 'stroke.dart';
import 'stroke_options.dart';
import 'package:testzooming/TestWidgetZoom.dart';
import 'package:perfect_freehand/perfect_freehand.dart';
import 'sketcher2.dart';
import 'stroke.dart';
import 'stroke_options.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
// const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, }) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var screenWidth = (window.physicalSize.shortestSide / window.devicePixelRatio);
var screenHeight = (window.physicalSize.longestSide / window.devicePixelRatio);
GlobalKey _boxKey = new GlobalKey();
Matrix4 _transform = Matrix4.identity();
final Offset _origin = Offset(0,0);
Offset _dragOffset = Offset.zero;
Offset correctedOffset = Offset.zero;
Offset _lastOffset = Offset.zero;
Offset boxOffset = Offset(0,0);
Offset finalOffset = Offset.zero;
double boxHeight = 0;
double boxLength = 0;
double safeOffsetdx = 0;
double scaleDistance = 0;
double _lastScale = 1;
double _lastScaleDistance = 1;
double _scale = 1;
double safeScale = 1;
int _counter = 0;
bool outside = false;
TouchCallbacks touchCallbacks = TouchCallbacks();
List<Stroke> lines = <Stroke>[];
Stroke line = Stroke( [], 1, Colors.red, .6, 1, .7, true, 1, 1, true, true, true, 1, Offset.zero);
StrokeOptions options = StrokeOptions();
StreamController<Stroke> currentLineStreamController = StreamController<Stroke>.broadcast();
StreamController<List<Stroke>> linesStreamController = StreamController<List<Stroke>>.broadcast();
bool firstTouch = false;
bool startPaint = false;
@override
void initState(){
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
Transform(
transform: _transform,
origin: Offset(MediaQuery.of(context).size.width/2,MediaQuery.of(context).size.height/2),
child: Container(
key: _boxKey,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
image: DecorationImage(
image: Image.network('https://i.pinimg.com/originals/99/a2/dc/99a2dcfa8eade86cdcc9ac747d75fae5.jpg').image
)
),
child: Stack(
fit: StackFit.expand,
children: [
RepaintBoundary(
child: Container(
alignment: Alignment.topLeft,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent
),
child: StreamBuilder<List<Stroke>>(
stream: linesStreamController.stream,
builder: (context, snapshot) {
return CustomPaint(
painter: Sketcher2(
lines: lines,
options: options,
),
);
},
),
),
),
RepaintBoundary(
child: Container(
alignment: Alignment.topLeft,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent
),
child: StreamBuilder<Stroke>(
stream: currentLineStreamController.stream,
builder: (context, snapshot) {
return CustomPaint(
painter: Sketcher2(
lines: line == null ? [] : [line],
options: options,
),
);
},
),
),
),
],
),
),
),
RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
ImmediateMultiDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(),
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = (Offset offset) {
//Touch Started
setState(() {
_counter++;
touchCallbacks.touchBegan(TouchData(_counter, offset));
});
if(touchCallbacks.taps.length == 2){
final RenderBox box = _boxKey.currentContext?.findRenderObject() as RenderBox;
boxOffset = box.localToGlobal(Offset.zero);
final boxsize = _boxKey.currentContext?.size;
boxLength = boxsize != null ? boxsize.width * _lastScale : 1;
boxHeight = boxsize != null ? boxsize.height * _lastScale : 1;
}
return ItemDrag((details, tId) {
//Touch Updated
if(touchCallbacks.taps.length == 2){
//zoom and drag function
double testScale = (((touchCallbacks.taps.first.offset - touchCallbacks.taps.last.offset).distance - touchCallbacks.startDistance)+ _lastScaleDistance)/40;
if(testScale>0 && testScale< 15) {
scaleDistance = ((touchCallbacks.taps.first.offset -
touchCallbacks.taps.last.offset).distance -
touchCallbacks.startDistance) +
_lastScaleDistance;
double pinchOriginX = touchCallbacks.firstTouch.dx;
double pinchOriginY = touchCallbacks.firstTouch.dy;
double transformOriginX = boxOffset.dx + boxLength / 2;
double transformOriginY = boxOffset.dy + boxHeight / 2;
double movement = scaleDistance - _lastScaleDistance;
// print(_lastScale);
double displacementX = (transformOriginX - pinchOriginX) / _lastScale;
double displacementY = (transformOriginY - pinchOriginY) / _lastScale;
//
correctedOffset = Offset(
_lastOffset.dx + ((displacementX * movement) / 40)
,
_lastOffset.dy + ((displacementY * movement) / 40)
);
_scale = scaleDistance / 40 + 1;
}
_dragOffset = Offset(
((((touchCallbacks.taps.first.offset.dx +
touchCallbacks.taps.last.offset.dx) / 2) -
touchCallbacks.firstTouch.dx)),
(((touchCallbacks.taps.first.offset.dy +
touchCallbacks.taps.last.offset.dy) / 2) -
touchCallbacks.firstTouch.dy));
finalOffset = _dragOffset + correctedOffset;
setState(() {
_transform = Matrix4(
_scale, 0, 0, 0, //
0, _scale, 0, 0, //
0, 0, 1, 0, //
finalOffset.dx, finalOffset.dy, 0, 1,
);
});
} else {
//paint method
if (firstTouch == false && touchCallbacks.taps.length < 2) {
// I used a bool to only call this only once every paint since i couldnt get the details for global position in the initial touch add part
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition) ;
final point = Point(offset.dx, offset.dy);
final points = [point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
setState(() {
firstTouch = true;
});
} else {
if (touchCallbacks.taps.length < 2) {
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition);
final point = Point(offset.dx, offset.dy);
final points = [...line.points, point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
}
}
}
setState(() {
touchCallbacks
.touchMoved(TouchData(tId, details.globalPosition));
});
}, (details, tId) {
//Touch Ended
if(touchCallbacks.taps.length == 2){
setState(() {
_lastOffset = finalOffset;
_lastScaleDistance = scaleDistance;
_lastScale = (scaleDistance/40) +1;
});
} else {
//add line to list of lines (drawing)
if(touchCallbacks.taps.length < 2) {
Stroke newline = Stroke(
line.points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset);
lines = List.from(lines)
..add(newline);
linesStreamController.add(lines);
setState(() {
firstTouch = false;
});
}
}
touchCallbacks
.touchEnded(TouchData(tId, const Offset(0, 0)));
}, (tId) {
touchCallbacks
.touchCanceled(TouchData(tId, const Offset(0, 0)));
}, _counter);
};
}),
},
),
],
),
);
}
}
class TouchCallbacks {
Offset firstTouch = Offset.zero;
double startDistance = 0;
List<TouchData> taps = []; //list that holds ongoing taps or drags
void touchBegan(TouchData touch) {
taps.add(touch);
if(taps.length == 2){
firstTouch = Offset((taps.first.offset.dx + taps.last.offset.dx)/2, (taps.first.offset.dy + taps.last.offset.dy)/2);
startDistance = (taps.first.offset - taps.last.offset).distance;
}
}
void touchMoved(TouchData touch) {
for (int i = 0; i < taps.length; i++) {
if (taps[i].touchId == touch.touchId) {
taps[i] = touch;
break;
}
}
}
void touchCanceled(TouchData touch) {
//touch canceled code here
taps.removeWhere((element) => element.touchId == touch.touchId);
}
void touchEnded(TouchData touch) {
//touch ended code here
taps.removeWhere((element) => element.touchId == touch.touchId);
if(taps.length < 2){
startDistance = 0;
}
}
}
class TouchData {
final int touchId;
final Offset offset;
TouchData(this.touchId, this.offset);
}
class ItemDrag extends Drag {
final Function onUpdate;
final Function onEnd;
final Function onCancel;
final int touchId;
ItemDrag(this.onUpdate, this.onEnd, this.onCancel, this.touchId);
@override
void update(DragUpdateDetails details) {
super.update(details);
onUpdate(details, touchId);
}
@override
void end(DragEndDetails details) {
super.end(details);
onEnd(details, touchId);
}
@override
void cancel() {
super.cancel();
onCancel(touchId);
}
}
感谢任何尝试的人为了帮助我。
以防万一有人提出这一点,从我看的互动观众看来,似乎不适合绘画。
如果要复制它,这是我用于绘制的软件包: https://pub.dev/packages/ppackages/ppackages/perfect_fect_freehhand
这就是它的样子:
So I'm working on a drawing app using Flutter. I want to implement pinch-zoom while still being able to paint on the canvas with one finger. I've figured out how to use an ImmediateMultiDragGestureRecognizer
to tell how many fingers are on the screen and then use my own zoom function if there are two fingers on the screen and paint on the canvas when there is only one. Everything works perfectly with a scale of 1.
My problem is that after I zoom in and try to paint again, the drawing is offset to the top left of the canvas and it gets worse the further I zoom in.
I am calculating the zoom offset and drag offset during the zoom and then passing it to the canvas background which is wrapped with a Transform
widget (this part works fine). Then when there is only one finger on the screen and the points are sent to the custom painter I also pass in the scale and offset that I pass to the canvas to give it the same offset and scale.
I've gotten this method to work before without the 2 finger detection but the gesture detector was also wrapped in the Transform widget which it's not here.
Here is my code for the gesture detector with the zoom and paint methods:
GestureRecognizerFactoryWithHandlers<
ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(),
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = (Offset offset) {
//Touch Started
setState(() {
_counter++;
touchCallbacks.touchBegan(TouchData(_counter, offset));
});
if(touchCallbacks.taps.length == 2){
final RenderBox box = _boxKey.currentContext?.findRenderObject() as RenderBox;
boxOffset = box.localToGlobal(Offset.zero);
final boxsize = _boxKey.currentContext?.size;
boxLength = boxsize != null ? boxsize.width * _lastScale : 1;
boxHeight = boxsize != null ? boxsize.height * _lastScale : 1;
}
return ItemDrag((details, tId) {
//Touch Updated
if(touchCallbacks.taps.length == 2){
//zoom and drag function
double testScale = (((touchCallbacks.taps.first.offset - touchCallbacks.taps.last.offset).distance - touchCallbacks.startDistance)+ _lastScaleDistance)/40;
if(testScale>0 && testScale< 15) {
scaleDistance = ((touchCallbacks.taps.first.offset -
touchCallbacks.taps.last.offset).distance -
touchCallbacks.startDistance) +
_lastScaleDistance;
double pinchOriginX = touchCallbacks.firstTouch.dx;
double pinchOriginY = touchCallbacks.firstTouch.dy;
double transformOriginX = boxOffset.dx + boxLength / 2;
double transformOriginY = boxOffset.dy + boxHeight / 2;
double movement = scaleDistance - _lastScaleDistance;
// print(_lastScale);
double displacementX = (transformOriginX - pinchOriginX) / _lastScale;
double displacementY = (transformOriginY - pinchOriginY) / _lastScale;
//
correctedOffset = Offset(
_lastOffset.dx + ((displacementX * movement) / 40)
,
_lastOffset.dy + ((displacementY * movement) / 40)
);
_scale = scaleDistance / 40 + 1;
}
_dragOffset = Offset(
((((touchCallbacks.taps.first.offset.dx +
touchCallbacks.taps.last.offset.dx) / 2) -
touchCallbacks.firstTouch.dx)),
(((touchCallbacks.taps.first.offset.dy +
touchCallbacks.taps.last.offset.dy) / 2) -
touchCallbacks.firstTouch.dy));
finalOffset = _dragOffset + correctedOffset;
setState(() {
_transform = Matrix4(
_scale, 0, 0, 0, //
0, _scale, 0, 0, //
0, 0, 1, 0, //
finalOffset.dx, finalOffset.dy, 0, 1,
);
});
} else {
//paint method
if (firstTouch == false && touchCallbacks.taps.length < 2) {
// I used a bool to only call this only once every paint since i couldnt get the details for global position in the initial touch add part
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition) ;
final point = Point(offset.dx, offset.dy);
final points = [point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
setState(() {
firstTouch = true;
});
} else {
if (touchCallbacks.taps.length < 2) {
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition);
final point = Point(offset.dx, offset.dy);
final points = [...line.points, point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
}
}
}
setState(() {
touchCallbacks
.touchMoved(TouchData(tId, details.globalPosition));
});
}, (details, tId) {
//Touch Ended
if(touchCallbacks.taps.length == 2){
setState(() {
_lastOffset = finalOffset;
_lastScaleDistance = scaleDistance;
_lastScale = (scaleDistance/40) +1;
});
} else {
//add line to list of lines (drawing)
if(touchCallbacks.taps.length < 2) {
Stroke newline = Stroke(
line.points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset);
lines = List.from(lines)
..add(newline);
linesStreamController.add(lines);
setState(() {
firstTouch = false;
});
}
}
touchCallbacks
.touchEnded(TouchData(tId, const Offset(0, 0)));
}, (tId) {
touchCallbacks
.touchCanceled(TouchData(tId, const Offset(0, 0)));
}, _counter);
};
}),
Here is my code for the CustomPainter
I use:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:perfect_freehand/perfect_freehand.dart';
import 'stroke.dart';
import 'stroke_options.dart';
class Sketcher2 extends CustomPainter {
final List<Stroke> lines;
final StrokeOptions options;
Sketcher2({required this.lines, required this.options});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = options.color;
for (int i = 0; i < lines.length; ++i) {
final outlinePoints = getStroke(
lines[i].points,
size: lines[i].size,
thinning: lines[i].thinning,
smoothing: lines[i].smoothing,
streamline: lines[i].streamline,
taperStart: lines[i].taperStart,
capStart: lines[i].capStart,
taperEnd: lines[i].taperEnd,
capEnd: lines[i].capEnd,
simulatePressure: lines[i].simulatePressure,
isComplete:lines[i].isComplete,
);
final path = Path();
Offset offset = Offset(-1*lines[i].offset.dx, -1*lines[i].offset.dy);
double scale = 1/lines[i].scale;
if (outlinePoints.isEmpty) {
return;
} else if (outlinePoints.length < 2) {
// If the path only has one line, draw a dot.
paint.color = lines[i].color;
path.addOval(Rect.fromCircle(
center: Offset((outlinePoints[0].x+offset.dx) * scale, (outlinePoints[0].y+offset.dy) * scale), radius: 1));
} else {
// Otherwise, draw a line that connects each point with a curve.
path.moveTo((outlinePoints[0].x+offset.dx) * scale, (outlinePoints[0].y+offset.dy) * scale);
for (int i = 1; i < outlinePoints.length - 1; ++i) {
final p0 = outlinePoints[i];
final p1 = outlinePoints[i + 1];
path.quadraticBezierTo(
(p0.x+offset.dx) * scale, (p0.y+offset.dy) * scale, ((p0.x+offset.dx) * scale + (p1.x+offset.dx) * scale) / 2, ((p0.y+offset.dy) * scale + (p1.y+offset.dy) * scale) / 2);
}
}
paint.color = lines[i].color;
canvas.drawPath(path, paint);
}
}
@override
bool shouldRepaint(Sketcher2 oldDelegate) {
return true;
}
}
and this is my full code for the drawing page:
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:perfect_freehand/perfect_freehand.dart';
import 'dart:ui';
import 'sketcher2.dart';
import 'stroke.dart';
import 'stroke_options.dart';
import 'package:testzooming/TestWidgetZoom.dart';
import 'package:perfect_freehand/perfect_freehand.dart';
import 'sketcher2.dart';
import 'stroke.dart';
import 'stroke_options.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
// const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, }) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var screenWidth = (window.physicalSize.shortestSide / window.devicePixelRatio);
var screenHeight = (window.physicalSize.longestSide / window.devicePixelRatio);
GlobalKey _boxKey = new GlobalKey();
Matrix4 _transform = Matrix4.identity();
final Offset _origin = Offset(0,0);
Offset _dragOffset = Offset.zero;
Offset correctedOffset = Offset.zero;
Offset _lastOffset = Offset.zero;
Offset boxOffset = Offset(0,0);
Offset finalOffset = Offset.zero;
double boxHeight = 0;
double boxLength = 0;
double safeOffsetdx = 0;
double scaleDistance = 0;
double _lastScale = 1;
double _lastScaleDistance = 1;
double _scale = 1;
double safeScale = 1;
int _counter = 0;
bool outside = false;
TouchCallbacks touchCallbacks = TouchCallbacks();
List<Stroke> lines = <Stroke>[];
Stroke line = Stroke( [], 1, Colors.red, .6, 1, .7, true, 1, 1, true, true, true, 1, Offset.zero);
StrokeOptions options = StrokeOptions();
StreamController<Stroke> currentLineStreamController = StreamController<Stroke>.broadcast();
StreamController<List<Stroke>> linesStreamController = StreamController<List<Stroke>>.broadcast();
bool firstTouch = false;
bool startPaint = false;
@override
void initState(){
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
Transform(
transform: _transform,
origin: Offset(MediaQuery.of(context).size.width/2,MediaQuery.of(context).size.height/2),
child: Container(
key: _boxKey,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
image: DecorationImage(
image: Image.network('https://i.pinimg.com/originals/99/a2/dc/99a2dcfa8eade86cdcc9ac747d75fae5.jpg').image
)
),
child: Stack(
fit: StackFit.expand,
children: [
RepaintBoundary(
child: Container(
alignment: Alignment.topLeft,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent
),
child: StreamBuilder<List<Stroke>>(
stream: linesStreamController.stream,
builder: (context, snapshot) {
return CustomPaint(
painter: Sketcher2(
lines: lines,
options: options,
),
);
},
),
),
),
RepaintBoundary(
child: Container(
alignment: Alignment.topLeft,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent
),
child: StreamBuilder<Stroke>(
stream: currentLineStreamController.stream,
builder: (context, snapshot) {
return CustomPaint(
painter: Sketcher2(
lines: line == null ? [] : [line],
options: options,
),
);
},
),
),
),
],
),
),
),
RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
ImmediateMultiDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(),
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = (Offset offset) {
//Touch Started
setState(() {
_counter++;
touchCallbacks.touchBegan(TouchData(_counter, offset));
});
if(touchCallbacks.taps.length == 2){
final RenderBox box = _boxKey.currentContext?.findRenderObject() as RenderBox;
boxOffset = box.localToGlobal(Offset.zero);
final boxsize = _boxKey.currentContext?.size;
boxLength = boxsize != null ? boxsize.width * _lastScale : 1;
boxHeight = boxsize != null ? boxsize.height * _lastScale : 1;
}
return ItemDrag((details, tId) {
//Touch Updated
if(touchCallbacks.taps.length == 2){
//zoom and drag function
double testScale = (((touchCallbacks.taps.first.offset - touchCallbacks.taps.last.offset).distance - touchCallbacks.startDistance)+ _lastScaleDistance)/40;
if(testScale>0 && testScale< 15) {
scaleDistance = ((touchCallbacks.taps.first.offset -
touchCallbacks.taps.last.offset).distance -
touchCallbacks.startDistance) +
_lastScaleDistance;
double pinchOriginX = touchCallbacks.firstTouch.dx;
double pinchOriginY = touchCallbacks.firstTouch.dy;
double transformOriginX = boxOffset.dx + boxLength / 2;
double transformOriginY = boxOffset.dy + boxHeight / 2;
double movement = scaleDistance - _lastScaleDistance;
// print(_lastScale);
double displacementX = (transformOriginX - pinchOriginX) / _lastScale;
double displacementY = (transformOriginY - pinchOriginY) / _lastScale;
//
correctedOffset = Offset(
_lastOffset.dx + ((displacementX * movement) / 40)
,
_lastOffset.dy + ((displacementY * movement) / 40)
);
_scale = scaleDistance / 40 + 1;
}
_dragOffset = Offset(
((((touchCallbacks.taps.first.offset.dx +
touchCallbacks.taps.last.offset.dx) / 2) -
touchCallbacks.firstTouch.dx)),
(((touchCallbacks.taps.first.offset.dy +
touchCallbacks.taps.last.offset.dy) / 2) -
touchCallbacks.firstTouch.dy));
finalOffset = _dragOffset + correctedOffset;
setState(() {
_transform = Matrix4(
_scale, 0, 0, 0, //
0, _scale, 0, 0, //
0, 0, 1, 0, //
finalOffset.dx, finalOffset.dy, 0, 1,
);
});
} else {
//paint method
if (firstTouch == false && touchCallbacks.taps.length < 2) {
// I used a bool to only call this only once every paint since i couldnt get the details for global position in the initial touch add part
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition) ;
final point = Point(offset.dx, offset.dy);
final points = [point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
setState(() {
firstTouch = true;
});
} else {
if (touchCallbacks.taps.length < 2) {
final box = context.findRenderObject() as RenderBox;
final offset = box.globalToLocal(details.globalPosition);
final point = Point(offset.dx, offset.dy);
final points = [...line.points, point];
line = Stroke(
points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset
);
currentLineStreamController.add(line);
}
}
}
setState(() {
touchCallbacks
.touchMoved(TouchData(tId, details.globalPosition));
});
}, (details, tId) {
//Touch Ended
if(touchCallbacks.taps.length == 2){
setState(() {
_lastOffset = finalOffset;
_lastScaleDistance = scaleDistance;
_lastScale = (scaleDistance/40) +1;
});
} else {
//add line to list of lines (drawing)
if(touchCallbacks.taps.length < 2) {
Stroke newline = Stroke(
line.points,
options.size,
options.color,
options.thinning,
options.smoothing,
options.streamline,
options.simulatePressure,
options.taperStart,
options.taperEnd,
options.capStart,
options.capEnd,
options.isComplete,
_lastScale,
finalOffset);
lines = List.from(lines)
..add(newline);
linesStreamController.add(lines);
setState(() {
firstTouch = false;
});
}
}
touchCallbacks
.touchEnded(TouchData(tId, const Offset(0, 0)));
}, (tId) {
touchCallbacks
.touchCanceled(TouchData(tId, const Offset(0, 0)));
}, _counter);
};
}),
},
),
],
),
);
}
}
class TouchCallbacks {
Offset firstTouch = Offset.zero;
double startDistance = 0;
List<TouchData> taps = []; //list that holds ongoing taps or drags
void touchBegan(TouchData touch) {
taps.add(touch);
if(taps.length == 2){
firstTouch = Offset((taps.first.offset.dx + taps.last.offset.dx)/2, (taps.first.offset.dy + taps.last.offset.dy)/2);
startDistance = (taps.first.offset - taps.last.offset).distance;
}
}
void touchMoved(TouchData touch) {
for (int i = 0; i < taps.length; i++) {
if (taps[i].touchId == touch.touchId) {
taps[i] = touch;
break;
}
}
}
void touchCanceled(TouchData touch) {
//touch canceled code here
taps.removeWhere((element) => element.touchId == touch.touchId);
}
void touchEnded(TouchData touch) {
//touch ended code here
taps.removeWhere((element) => element.touchId == touch.touchId);
if(taps.length < 2){
startDistance = 0;
}
}
}
class TouchData {
final int touchId;
final Offset offset;
TouchData(this.touchId, this.offset);
}
class ItemDrag extends Drag {
final Function onUpdate;
final Function onEnd;
final Function onCancel;
final int touchId;
ItemDrag(this.onUpdate, this.onEnd, this.onCancel, this.touchId);
@override
void update(DragUpdateDetails details) {
super.update(details);
onUpdate(details, touchId);
}
@override
void end(DragEndDetails details) {
super.end(details);
onEnd(details, touchId);
}
@override
void cancel() {
super.cancel();
onCancel(touchId);
}
}
Thanks a ton to anyone who tries to help me with this.
Just in case anyone suggests it, from what I've looked into interactive viewer doesn't seem to work with painting.
This is the package I used for drawing if you want to replicate it: https://pub.dev/packages/perfect_freehand
Here is what it looks like:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我建议用比例尺来分割,而不是乘以它。
I propose to divide by the scale instead of multiplying by it.