颤音:缩放画布在图纸应用中缩放帆布后的补偿

发布于 2025-02-12 13:27:45 字数 28451 浏览 0 评论 0原文

因此,我正在使用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:

enter image description here

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

蓝天 2025-02-19 13:27:46

我建议用比例尺来分割,而不是乘以它。

I propose to divide by the scale instead of multiplying by it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文