【问题标题】:Doing Undo and Redo with Cglayer Drawing使用 Cglayer 绘图进行撤消和重做
【发布时间】:2014-01-09 18:03:16
【问题描述】:

我正在使用绘图应用程序,我正在使用 CGlayers 进行绘图。在触摸结束时,我从图层中取出图像并将其存储在一个数组中,我用它来撤消操作。

我的触摸结束功能

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{    
    NSLog(@"Touches ended");

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawLayerInRect(context, self.bounds, self.drawingLayer);
    m_curImage = UIGraphicsGetImageFromCurrentImageContext();  
    UIGraphicsEndImageContext(); 

    [m_undoArray addObject: m_curImage];
}

我的绘图视图根据用户需求动态扩展,因此假设用户可以绘制一条 drawView 大小为 200*200 的线,然后将其扩展为 200*300 并再绘制一条线,然后将其扩展为 200*300 并绘制多一条线。

这是应用程序的图像

所以现在我在 UndoArray 中有 3 个不同大小的图像。

每当我增加/减少画布大小时。我已经写了这段代码

关于drawingView的增减,我在写这个函数

 (void)increaseDecreaseDrawingView 
{ 
self.currentDrawingLayer = nil; 

if(self.permanentDrawingLayer) 
{ 
rectSize = self.bounds; 
NSLog(@"Size%@", NSStringFromCGRect(self.bounds)); 
CGContextRef context = UIGraphicsGetCurrentContext(); 

//self.newDrawingLayer = CGLayerCreateWithContext(context, self.bounds.size, NULL); 
CGFloat scale = self.contentScaleFactor; 
CGRect bounds = CGRectMake(0, 0, self.bounds.size.width * scale, self.bounds.size.height * scale); 
CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL); 
CGContextRef layerContext = CGLayerGetContext(layer); 
CGContextScaleCTM(layerContext, scale, scale); 
self.newDrawingLayer = layer; 


CGContextDrawLayerInRect(layerContext, self.bounds, self.permanentDrawingLayer ); 

self.permanentDrawingLayer = nil; 

} 

为了撤消,我写了这段代码

- (void)Undo
{
     //Destroy the layer and create it once again with the image you get from undoArray.
     self.currentDrawingLayer = Nil;

     CGContextRef layerContext1 = CGLayerGetContext(self.permanentDrawingLayer );
     CGContextClearRect(layerContext1, self.bounds);

     CGContextRef context = UIGraphicsGetCurrentContext();

    for(int i =0; i<[m_rectArrayUndo count];i++)
    {
        CGRect rect = [[m_rectArrayUndo objectAtIndex:i]CGRectValue];
        CGLayerRef undoLayer = CGLayerCreateWithContext(context, rect.size, NULL);

        CGContextRef layerContext = CGLayerGetContext(undoLayer );
        CGContextTranslateCTM(layerContext, 0.0, rect.size.height);
        CGContextScaleCTM(layerContext, 1.0, -1.0);

        CGRect imageFrame;

        NSDictionary *lineInfo = [m_undoArray objectAtIndex:i];
        m_curImage = [lineInfo valueForKey:@"IMAGE"];
       imageFrame = CGRectMake(0 ,0,m_curImage.size.width,m_curImage.size.height);
       CGContextDrawImage(layerContext, imageFrame, m_curImage.CGImage);
       CGContextDrawLayerInRect(context, rect, undoLayer );
       CGContextDrawLayerInRect(layerContext1, rect, undoLayer);
    }          
}

在我的drawRect函数中,我写了这段代码

- (void)drawRect:(CGRect)rect
{    

            CGContextRef context = UIGraphicsGetCurrentContext();//Get a reference to current context(The context to draw)

            if(self.currentDrawingLayer == nil)
            {                
                CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL);                             
                self.currentDrawingLayer = layer;
            }



            if(self.permanentDrawingLayer == nil)
            {
                CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL);
                self.permanentDrawingLayer = layer;
            }


            if(self.newDrawingLayer == nil)
            {
                CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL);
                self.newDrawingLayer = layer;
            }

            CGPoint mid1 = midPoint(m_previousPoint1, m_previousPoint2);
            CGPoint mid2 = midPoint(m_currentPoint, m_previousPoint1);



            CGContextRef layerContext = CGLayerGetContext(self.currentDrawingLayer);

            CGContextSetLineCap(layerContext, kCGLineCapRound);
            CGContextSetBlendMode(layerContext, kCGBlendModeNormal);
            CGContextSetLineJoin(layerContext, kCGLineJoinRound);
            CGContextSetLineWidth(layerContext, self.lineWidth);
            CGContextSetStrokeColorWithColor(layerContext, self.lineColor.CGColor);
            CGContextSetShouldAntialias(layerContext, YES);
            CGContextSetAllowsAntialiasing(layerContext, YES);
            CGContextSetAlpha(layerContext, self.lineAlpha);
            CGContextSetFlatness(layerContext, 1.0f);
            CGContextBeginPath(layerContext);
            CGContextMoveToPoint(layerContext, mid1.x, mid1.y);//Position the current point
            CGContextAddQuadCurveToPoint(layerContext, m_previousPoint1.x, m_previousPoint1.y, mid2.x, mid2.y);
            CGContextStrokePath(layerContext);//paints(fills) the line along the current path.

            CGContextDrawLayerInRect(context, self.bounds, self.newDrawingLayer);

            CGContextDrawLayerInRect(context,  self.bounds, self.permanentDrawingLayer);
            CGContextDrawLayerInRect(context, self.bounds, self.currentDrawingLayer);

            [super drawRect:rect];
}

我没有什么疑问

  1. 这是正确的做法吗?还是他们有更好的方法。

  2. 这里发生的情况是,我的撤消数组中的图像不尊重矩形,而是在新层上的任何随机位置绘制。

所以我想知道我们如何正确地绘制它们,以便在特定位置的 CGlayers 上正确地绘制图像。

【问题讨论】:

  • 为每个用户操作存储一个图像似乎是个坏主意。这将快速咀嚼记忆。为什么不存储用户输入和/或绘图命令,并在撤消时重新创建图像。如果需要速度,请将最后的 N 撤消存储为图像,然后从命令重新创建。
  • @iccir,我没听懂你,你能详细说明一下

标签: ios uikit core-graphics cgcontext cglayer


【解决方案1】:

首先,由于您使用的是图层,我建议放弃drawRect:,而只使用CALayer 转换。

其次,在我看来,实现撤消重做操作的最佳方式始终是基于命令的。作为一个非常简单的示例,您可以为每个命令创建单独的方法:

- (void)scaleLayerBy:(CGFloat)scale;
- (void)moveLayerByX:(CGFloat)x Y:(CGFloat)y;
// etc

然后每次用户进行操作时,您将操作 ID 和参数添加到 NSMutableArray

[self.actionHistory addObject:@{ @"action": @"move", @"args": @[@10.0f, @20.0f] }];

相反,如果用户调用 undo,则删除该数组中的最后一个对象。

然后当您需要重新加载显示时,只需重新评估数组中的所有命令即可。

[self resetLayers]; // reset CALayers to their initial state
for (NSDictionary *command in self.actionHistory) {
    NSArray *arguments = command[@"args"];
    if ([command[@"action"] isEqualToString:@"move"]) {
        [self moveLayerByX:[arguments[0] floatValue] Y:[arguments[1] floatValue]];
    }
    // else if other commands
}

【讨论】:

  • 谢谢您的评论,我正在使用 CGlayers,因为我想根据某些条件清除一些绘图。我可以用 CAlayers 做到这一点吗?
  • 当然。只需添加/删除子子层。 CALayers 的显示性能比处理原始图纸的性能更高。有很多种专门的图层,例如CAShapeLayer,您可以使用它们来绘制路径,或者如果您需要按原样显示位图,您可以将CGImageRefs 分配给layer.content。用户完成编辑后,您可以使用 -renderInContext: 或通过评估操作历史记录中的命令来绘制整个 CALayer 树。
  • 好吧。我还有一个疑问,为什么你建议我不要使用 CGLayers?
  • 因为CALayers 直接与视图一起工作,而无需您担心绘图。您所要做的就是保留动作和参数的历史记录,然后设置图层的各种属性(例如frameboundstransformpathlineWidthlineCap、等)从这些参数。您还可以大大提高性能,因为您不需要在内存中保留许多绘制的图像。
  • 你好@JohnEstropia,与此同时,当我在处理其他代码时,我无法完成这项任务。现在我回来破解它。所以我对你的回答感到困惑。你能再解释一下吗?
【解决方案2】:

每个触摸事件的图像对象是一个坏主意恕我直言,你正在撕裂 ram。为什么不保留一系列接触点并动态绘制呢?很容易从该数组中删除最后几个元素以进行廉价的撤消操作

////2014 年 1 月 14 日// //编辑以包含示例//

好的,这里是一个快速绘图视图示例。 有三个可变数组,_touches,用于所有以前的绘图,_currentTouch,它是当前绘图,仅包含触摸事件期间的数据,(在触摸开始和触摸结束之间).. 和一个重做数组,该数据被撤消删除被复制到而不是仅仅删除它(你当然可以这样做)

享受:)

//
//  JEFdrawingViewExample.m
//  Created by Jef Long on 14/01/2014.
//  Copyright (c) 2014 Jef Long / Dragon Ranch. All rights reserved.
//

#import "JEFdrawingViewExample.h"
///don't worry, the header is empty :)
/// this is a subclass of UIView

@interface JEFdrawingViewExample()

-(UIColor *)colourForLineAtIndex:(int)lineIndex;
//swaps the coulour for each line

-(void)undo;
-(void)redo;

@end;


@implementation JEFdrawingViewExample
{
//iVars
  NSMutableArray *_touches;
  NSMutableArray *_currentTouch;
  NSMutableArray *_redoStore;
  }

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
      _touches = [[NSMutableArray alloc]init];
      _currentTouch = [[NSMutableArray alloc]init];
      _redoStore = [[NSMutableArray alloc]init];
    }
    return self;
}

#pragma mark - touches
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
  UITouch *touch = [touches anyObject];
  CGPoint touchPoint = [touch locationInView:self];

  [_currentTouch removeAllObjects];

  [_currentTouch addObject:NSStringFromCGPoint(touchPoint)];
  ///there are other, possibly less expensive ways to do this.. (adding a CGPoint to an NSArray.)
  // typecasting to (id) doesnt work under ARC..
  // two NSNumbers probably not any cheaper..

}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{

  UITouch *touch = [touches anyObject];
  CGPoint touchPoint = [touch locationInView:self];
  [_currentTouch addObject:NSStringFromCGPoint(touchPoint)];
  [self setNeedsDisplay];
  }

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{

  UITouch *touch = [touches anyObject];
  CGPoint touchPoint = [touch locationInView:self];
  [_currentTouch addObject:NSStringFromCGPoint(touchPoint)];
  [_touches addObject:[NSArray arrayWithArray:_currentTouch]];
  [_currentTouch removeAllObjects];
  [self setNeedsDisplay];
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{

  [_currentTouch removeAllObjects];
  [self setNeedsDisplay];
}






#pragma mark - drawing
- (void)drawRect:(CGRect)rect
{

  //we could be adding a CALayer for each new line, which would be cheaper because you could draw each and basically forget it

  CGContextRef _context = UIGraphicsGetCurrentContext();
  CGContextSetLineWidth(_context, 1.0);  //or whatever


///older lines
  if ([_touches count]) {
  for (int line = 0; line < [_touches count]; line ++) {


    NSArray *thisLine = [_touches objectAtIndex:line];
    if ([thisLine count]) {

      CGContextSetStrokeColorWithColor(_context, [self colourForLineAtIndex:line].CGColor);
      CGPoint start = CGPointFromString([thisLine objectAtIndex:0]);
      CGContextMoveToPoint(_context, start.x, start.y);

    for (int touch = 1; touch < [thisLine count]; touch ++) {
      CGPoint pt = CGPointFromString([thisLine objectAtIndex:touch]);
      CGContextAddLineToPoint(_context, pt.x, pt.y);

    }
      CGContextStrokePath(_context);
    }

  }


  }
///current line
  if ([_currentTouch count]) {
    CGPoint start = CGPointFromString([_currentTouch objectAtIndex:0]);
    CGContextSetStrokeColorWithColor(_context, [self colourForLineAtIndex:[_touches count]].CGColor);
    CGContextMoveToPoint(_context, start.x, start.y);
    for (int touch = 1; touch < [_currentTouch count]; touch ++) {

      CGPoint touchPoint = CGPointFromString([_currentTouch objectAtIndex:touch]);
      CGContextAddLineToPoint(_context, touchPoint.x, touchPoint.y);

    }
    CGContextStrokePath(_context);
  }
}

-(UIColor *)colourForLineAtIndex:(int)lineIndex{

  return (lineIndex%2 == 0) ? [UIColor yellowColor] : [UIColor purpleColor];

  /// you might have a diff colour for each line, eg user might select a pencil from a toolbar etc
}


#pragma mark - undo mechanism
-(void)undo{

  if ([_currentTouch count]) {

    [_redoStore addObject:[NSArray arrayWithArray:_currentTouch]];
    [_currentTouch removeAllObjects];
    [self setNeedsDisplay];

  }else if ([_touches count]){

    [_redoStore addObject:[_touches lastObject]];
    [_touches removeLastObject];
    [self setNeedsDisplay];


  }else{
  //nothing left to undo
  }
}

-(void)redo{
  if ([_redoStore count]) {

    [_touches addObject:[_redoStore lastObject]];
    [_redoStore removeLastObject];
    [self setNeedsDisplay];

  }

}

@end

【讨论】:

  • 你能详细说明一下吗?我没有得到你
  • 你好@Jef,请看一下这个,stackoverflow.com/questions/21438586/…我在类似的线路上尝试过一些东西,我需要你的帮助
  • 好的,伙计。我会直接点击.. 只是为那些看过上面我做的快速小类的人添加评论,有评论肯定有比 NSString 更便宜的方法来将 cgpoint 存储在对象中,我最近学习了 NSValue也有往返 cgpoint 的便利转换器,可能比大型数组的字符串更有效,尽管它不会像 NSString 那样容易写入文件,它可能会在 ram 上更轻
  • 谢谢@Jef,我正在等待您在我的帖子中回复
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-30
  • 1970-01-01
  • 2023-03-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多