Doing Undo and Redo with Cglayer Drawing
Asked Answered
A

2

3

I am working with a drawing app, I am using CGlayers for drawing. On touches ended, I get image out of the layer and store it in a Array, which I use to undo operation.

My touches ended function

-(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];
}

My Drawing View expands dynamically on user demand, So suppose user can draw say, a line with drawView size 200*200, then expand it to 200*300 and draw one more line, then expand it to 200*300 and draw one more line.

Here is the image of the app

So now I have 3 images with different sizes in UndoArray.

Whenever I increase/decrese the canvas size. I have written this code

On increase and decrease of the drawingView, I am writing this function

 (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; 

} 

And for doing undo I have written this code

- (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);
    }          
}

In my drawRect function, I have written this code

- (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];
}

I have few doubts

  1. Is this the correct way to do? Or is their any better approach.

  2. Here What happens is that, my images from undo array are not respecting the rects and are drawn at any random position on the new Layer.

So I want to know how we can draw them properly so that images are drawn properly on CGlayers at specific position.

Austine answered 9/1, 2014 at 18:3 Comment(2)
Storing one image for each user operation seems like a bad idea. That's going to chew through memory quickly. Why not store the user input and/or drawing commands, and recreate the image on undo. If speed is needed, store the last N undos as images, then recreate from commands.Saudra
@iccir, I didnt get you, Can you elaborate itAustine
M
5

First of all, since you are working with layers, I suggest give up on drawRect: and just work with CALayer transforms.

Second, in my opinion, the best way to implement undo-redo operations will always be command-based. As a very simple example, you can make separate methods for each command:

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

And then each time the user makes an action, you add to an NSMutableArray the action id and the parameters:

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

Conversely, if the user invokes undo, remove the last object in that array.

Then when you need to reload the display, just reevaluate all the commands in the array.

[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
}
Marmoreal answered 20/1, 2014 at 2:9 Comment(7)
Hey thanks for your comment, I am using CGlayers because I want to clear out some drawing based on some conditions. Can I do that with CAlayers?Austine
Sure. Just add/remove children sublayers. CALayers are more performant for displaying than working with raw drawings. There are many kinds of specialized layers such as CAShapeLayer which you can use to draw your paths, or if you need to display bitmaps as is you can assign CGImageRefs to layer.content. Once the user has finalized his/her edits, you can draw the whole CALayer tree using -renderInContext:, or by evaluating the commands in the action history.Marmoreal
Ok.I have one more doubt, why you suggest me not use CGLayers?Austine
Because CALayers work directly with the views without the need for you to worry about drawing. All you have to do is keep the history of actions and parameters, then set the layers' wide range of properties (such as frame, bounds, transform, path, lineWidth, lineCap, etc) from those parameters. You also greatly improve performance because you don't need to keep many drawn images in memory.Marmoreal
Hello @JohnEstropia, in the mean while as I was working on other code, I could not manage to complete this task. Now I am back to crack it. So I am confused with your answer. Can you explain me once again.Austine
Hello @JohnEstropia, please have a look at this #21439086 I need your helpAustine
Hello @JohnEstropia please help me out https://mcmap.net/q/331846/-multitouch-tracking-issueAustine
D
2

An image object for each touch event is a bad idea IMHO, you're tearing through ram. Why not keep an array of touch points and draw dynamically? Easy enough to remove the last few elements from that array for a cheap undo operation

////14 Jan 2014// //edit to include example//

OK here is a quick drawing view example. there are three mutableArrays, _touches, which is for all previous drawings, _currentTouch, which is the current drawing and only contains data during touch events, (between touches began and touches ended).. and a redo array that data which is removed by undo is copied to rather than just deleting it (which you can certainly do)

enjoy :)

//
//  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
Dairymaid answered 14/1, 2014 at 1:18 Comment(5)
Can you elaborate it. I didt get youAustine
Hello @Jef, please have a look at this, #21439086 I have tried something on similar lines, I need your helpAustine
Sure mate. I'll click right over.. Just adding a comment for people who look at the above quick little class I did, there is a comment there must be a cheaper way than NSString to store a cgpoint inside an object, well I recently learned NSValue has convenience converters to and from cgpoint as well, probably more efficient than string for large arrays, although it won't write to file as easily as NSString it'll probably be lighter on ramDairymaid
Thanks @Jef, I am waiting for your answer on my postAustine
please help me out https://mcmap.net/q/331846/-multitouch-tracking-issueAustine

© 2022 - 2024 — McMap. All rights reserved.