转换绘图视图时iOS绘图出错

Posted

技术标签:

【中文标题】转换绘图视图时iOS绘图出错【英文标题】:iOS drawing goes wrong when drawing view is transformed 【发布时间】:2014-11-19 22:00:38 【问题描述】:

我有一个带有 UIPanGestureRecogniser 的自定义 UIView,用于捕获点和速度。我将这些点和速度存储在 NSMutableArrays 中,然后使用数据创建 UIBezierPaths,然后将其添加到撤消/重做历史记录中。

当用户在屏幕上移动手指时,撤消历史会不断添加新路径,并且每条新路径都被绘制到屏幕外的图形上下文中,裁剪(到路径的大小)然后在 UIView 上绘制。

我的问题是:现在我正在创建一个捏到缩放功能并将缩放和平移变换应用于绘图视图,任何在放大时完成的绘图最终都会出现在错误的位置(它向上和手指在屏幕上的左侧)和错误的尺寸(更小)。在撤消和重做之前,您实际上无法看到您正在绘制的内容。我在想屏幕外上下文或屏幕的错误矩形正在更新,或者由于变换而存储在撤消历史记录中的路径点具有不同的原点/参考点(不知道该怎么称呼它!)。

这是我的第一个 ios 应用程序(可能存在一些愚蠢的错误),我已经使用了许多不同的教程来做到这一点,但我对变换如何影响路径感到困惑。以及如何确保路径以正确的比例绘制在屏幕外上下文的正确位置。我尝试过变换路径、变换点并尝试反转变换,但我就是不明白。

所以这里是代码(为数量道歉)。我包括处理捕捉点、制作路径和在屏幕上绘图的内容……捏识别器将放大绘图视图,并将其转换为将其放大到捏的中心。

在 ViewController 中,我以整个屏幕的大小创建绘图视图 (VelocityDrawer) 并添加手势识别器:

VelocityDrawer *slv = [[VelocityDrawer alloc] initWithFrame:CGRectMake(0,0,768,1024)];
slv.tag = 100;
drawingView = slv;

drawingView.delegate = self;
drawingView.currentPen = finePen;

然后initWithFrame:(CGRect)frame 在 VelocityDrawer 中:

    self.undoHistory = [[NSMutableArray alloc] init];
    self.redoHistory = [[NSMutableArray alloc] init];

    // create offscreen context
    drawingContext = [self createOffscreenContext:frame.size];

    UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
    panGestureRecognizer.maximumNumberOfTouches = 1;
    [self addGestureRecognizer:panGestureRecognizer];

    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    tapGestureRecognizer.numberOfTapsRequired = 1;
    tapGestureRecognizer.numberOfTouchesRequired = 1;
    [self addGestureRecognizer:tapGestureRecognizer];

    [self clearHistoryBitmaps];

屏幕外上下文是这样创建的:

- (CGContextRef) createOffscreenContext: (CGSize) size  
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    float scaleFactor = [[UIScreen mainScreen] scale];

    // must use whole numbers so invalid context does not happen
    NSInteger sw = (NSInteger)(size.width * scaleFactor);
    NSInteger sh = (NSInteger)(size.height * scaleFactor);

    CGContextRef context = CGBitmapContextCreate(NULL,
                                                 sw,
                                                 sh,
                                                 8,
                                                 sw * 4,
                                                 colorSpace,
                                                 (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
    CGColorSpaceRelease(colorSpace);

    CGContextScaleCTM(context, scaleFactor, scaleFactor);
    return context;

handlePanGesture。我捕获点,计算extractSize 中线的大小或宽度(基于手指速度),然后将信息添加到数组中:

    if (panGestureRecognizer.state == UIGestureRecognizerStateBegan)
    
        CGPoint point = [panGestureRecognizer locationInView:panGestureRecognizer.view];
        CGPoint prev  = [[points firstObject] pos];
        float size;

        size = currentPen.minWidth/2;

        // Add point to array
        [self addPoint:point withSize:size];

        // Add empty group to history
        [undoHistory addObject:[[NSMutableArray alloc] init]];
    

    if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) 

        CGPoint point = [panGestureRecognizer locationInView:panGestureRecognizer.view];
        currentPoint = [(LinePoint *)[points lastObject] pos];

        float size = clampf([self extractSize:panGestureRecognizer], currentPen.minWidth, currentPen.maxWidth);
       [self addPoint:point withSize:size];

        NSMutableArray *pArr = [[NSMutableArray alloc] init];
        UIBezierPath *sizer = [[UIBezierPath alloc] init];

        // interpolate points to make smooth variable width line 
        NSMutableArray *interPoints = [self calculateSmoothLinePoints];

        // code continues

我循环遍历interPoints(识别器中最新点和先前点之间的插值点数组)创建将在屏幕上绘制的路径:

        // other code here
        // loop starts

        CGMutablePathRef path = CGPathCreateMutable();
        CGPathMoveToPoint(path, NULL, mid1.x, mid1.y);

        CGPathAddQuadCurveToPoint(path, NULL, prevT1.x, prevT1.y, mid2.x, mid2.y);
        CGPathAddLineToPoint(path, NULL, mid2b.x, mid2b.y);

        CGPathAddQuadCurveToPoint(path, NULL, prevB1.x, prevB1.y, mid1b.x, mid1b.y);
        CGPathAddLineToPoint(path, NULL, mid1.x, mid1.y);

        UIBezierPath *aPath = [UIBezierPath bezierPath];
        aPath.CGPath = path;

        [sizer appendPath:aPath];
        [pArr addObject:aPath];

        // more code here
        // loop ends

        CGPathRelease(path);

将所有路径添加到pArr 后,我创建一个HistoryItem 并用路径、线条颜色、线条宽度等填充它。

    HistoryItem *action = [[HistoryItem alloc] initWithPaths:pArr
                                               andLineColour:self.lineColor
                                                andLineWidth:self.lineWidth
                                                 andDrawMode:self.currentDrawMode
                                                    andScale:self.scale];

    [self addAction:action];

addActionHistoryItem 添加到撤消堆栈。请注意,我记录了self.scale,但不做任何事情。然后我得到边界矩形(drawBox)并调用setNeedsDisplayInRect

    CGRect drawBox = CGPathGetBoundingBox(sizer.CGPath);

    //Pad bounding box to respect line width
    drawBox.origin.x        -= self.lineWidth * 1;
    drawBox.origin.y        -= self.lineWidth * 1;
    drawBox.size.width      += self.lineWidth * 2;
    drawBox.size.height     += self.lineWidth * 2;

    [self setNeedsDisplayInRect:drawBox];

当手势完成时,我在线条上添加一个圆形末端。代码省略。

最后绘制矩形:

- (void)drawRect:(CGRect)rect

    CGContextRef context = UIGraphicsGetCurrentContext();

    // offScreen context
    UIGraphicsPushContext(drawingContext);

    HistoryItem *action = [[undoHistory lastObject] lastObject];

    if(currentDrawMode == DRAW || currentDrawMode == ERASE) 
        for (UIBezierPath *p in action.pathsToDraw) 

            p.lineWidth = 1;
            p.lineCapStyle = kCGLineCapRound;

            [action.lineColor setFill];
            [action.lineColor setStroke];

            [p fill];
            [p stroke];
        
    
    if(currentDrawMode == UNDO) 
        CGContextClearRect(drawingContext, self.bounds);

        for (NSArray *actionGroup in undoHistory) 
            for (HistoryItem *undoAction in actionGroup) 
                for (UIBezierPath *p in undoAction.pathsToDraw) 
                    p.lineWidth = 1;
                    p.lineCapStyle = kCGLineCapRound;

                    [undoAction.lineColor setFill];
                    [undoAction.lineColor setStroke];

                    [p fill];
                    [p stroke];  
                
            
        
    
    // similar code for redo omitted

也许这里的框架/尺寸有问题?

    // Continuation of drawRect: 

    CGImageRef cgImage = CGBitmapContextCreateImage(drawingContext);
    CGContextClipToRect(context, rect);
    CGContextDrawImage(context, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height), cgImage);
    CGImageRelease(cgImage);

    [super drawRect:rect];

【问题讨论】:

你找到答案了吗?我有同样的问题,在任何地方都找不到合适的答案...... 【参考方案1】:

重绘线时,您需要对点应用相同的变换。您存储在数组中的点与其渲染的画布相关。我建议您存储原始帧,然后在下一行渲染中缩放原始帧和新尺寸之间的坐标。

【讨论】:

请您澄清一下“渲染它的画布”是什么意思?我已经尝试将绘图视图的变换应用于绘图视图的 drawRect 中的路径,但它并没有解决问题(它有所不同但仍然是错误的)。请您举例说明您将如何实施您的建议?

以上是关于转换绘图视图时iOS绘图出错的主要内容,如果未能解决你的问题,请参考以下文章

如何缩放 iOS 绘图应用程序?

在背景中绘图

iOS 绘图-UIBezierPath

iOS绘图事务的运行验证

iOS - 图形上下文绘图

IOS:用他的触摸和点击能力保存子视图