Stretchy UIBezierPath line?

2019-03-22 04:42发布

问题:

I want to draw a straight line with my finger that automatically sizes based upon how far away I am from the point of origin.

So if I touch the screen in the middle and slide my finger out a line appears to 'stretch' and pivot around the point of orgin as my finger moves on the screen. WHhen I lift my finger. The Destination Point should finalize and create a line. I can drag my finger across the screen and 'Draw' on the screen but that's not what I am wanting to do.

I thought UIBeizerPath moveToPoint would help but it just messes things up.

What am I doing wrong?

- (id)initWithFrame:(CGRect)frame
{
     //default line properties
    myPath=[[UIBezierPath alloc]init];
    myPath.lineCapStyle=kCGLineCapRound;
    myPath.miterLimit=0;
    myPath.lineWidth=lineWidth;
    brushPattern=[UIColor blackColor];
 }



-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    CGPoint curPoint = [[touches anyObject] locationInView:self];

    lastPoint = curPoint;

    [myPath moveToPoint:lastPoint];
    [pathArray addObject:myPath];
    [self setNeedsDisplay];
}

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

    CGPoint curPoint = [[touches anyObject] locationInView:self];

    myPath.lineWidth=lineWidth;
    brushPattern=[UIColor redColor]; //red to show it hasn't been added yet.
    [myPath moveToPoint:tempPoint];
    [self setNeedsDisplay];
}

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

    CGPoint curPoint = [[touches anyObject] locationInView:self];

    myPath.lineWidth=lineWidth;

    brushPattern=[UIColor blackColor]; //finalize the line with black color
    [myPath addLineToPoint:curPoint];
    [self setNeedsDisplay];
}

回答1:

Here's one concept. Draws a line from where you start dragging your finger until where you let go, animating it as you drag your finger around. It does this by making a CAShapeLayer, resetting the path as you move your finger around.

This should demonstrate the basic idea:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
    [self.view addGestureRecognizer:gesture];
}

- (CAShapeLayer *)createShapeLayer:(UIView *)view {
    CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];

    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.lineWidth = 3.0;

    [view.layer addSublayer:shapeLayer];

    return shapeLayer;
}

- (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
    static CAShapeLayer *shapeLayer;
    static CGPoint origin;

    if (gesture.state == UIGestureRecognizerStateBegan) {
        shapeLayer = [self createShapeLayer:gesture.view];
        origin = [gesture locationInView:gesture.view];
    } else if (gesture.state == UIGestureRecognizerStateChanged) {
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:origin];
        CGPoint location = [gesture locationInView:gesture.view];
        [path addLineToPoint:location];
        shapeLayer.path = path.CGPath;
    } else if (gesture.state == UIGestureRecognizerStateEnded ||
               gesture.state == UIGestureRecognizerStateFailed ||
               gesture.state == UIGestureRecognizerStateCancelled) {
        shapeLayer = nil;
    }
}

Or, in Swift 3:

override func viewDidLoad() {
    super.viewDidLoad()

    let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
    view.addGestureRecognizer(pan)
}

private func createShapeLayer(for view: UIView) -> CAShapeLayer {
    let shapeLayer = CAShapeLayer()
    shapeLayer.fillColor = UIColor.clear.cgColor
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.lineWidth = 3.0

    view.layer.addSublayer(shapeLayer)

    return shapeLayer
}

private var shapeLayer: CAShapeLayer!
private var origin: CGPoint!

func handlePan(_ gesture: UIPanGestureRecognizer) {
    if gesture.state == .began {
        shapeLayer = createShapeLayer(for: gesture.view!)
        origin = gesture.location(in: gesture.view)
    } else if gesture.state == .changed {
        let path = UIBezierPath()
        path.move(to: origin)
        path.addLine(to: gesture.location(in: gesture.view))
        shapeLayer.path = path.cgPath
    } else if gesture.state == .ended || gesture.state == .failed || gesture.state == .cancelled {
        shapeLayer = nil
    }
}

If you don't use CAShapeLayer, but you want to keep track of previous paths, you'll have to maintain an array for those old paths, and build a path that consists of all of the old paths, perhaps something like:

@interface CustomView ()

@property (nonatomic) CGPoint originPoint;
@property (nonatomic) CGPoint currentPoint;
@property (nonatomic) NSMutableArray *previousPaths;

@end

@implementation CustomView

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self configure];
    }
    return self;
}

- (id)init {
    return [self initWithFrame:CGRectZero];
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self configure];
    }
    return self;
}

- (void)configure {
    _previousPaths = [[NSMutableArray alloc] init];
}

- (void)drawRect:(CGRect)rect {
    [[UIColor redColor] setStroke];

    UIBezierPath *drawPath = [UIBezierPath bezierPath];

    drawPath.lineCapStyle = kCGLineCapRound;
    drawPath.miterLimit = 0;
    drawPath.lineWidth = 3.0;

    for (UIBezierPath *path in self.previousPaths)
        [drawPath appendPath:path];

    UIBezierPath *path = [self pathForCurrentLine];
    if (path)
        [drawPath appendPath:path];

    [drawPath stroke];
}

- (UIBezierPath *)pathForCurrentLine {
    if (CGPointEqualToPoint(self.originPoint, CGPointZero) && CGPointEqualToPoint(self.currentPoint, CGPointZero))
        return nil;

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:self.originPoint];
    [path addLineToPoint:self.currentPoint];

    return path;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.originPoint = [[touches anyObject] locationInView:self];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    if ([event respondsToSelector:@selector(predictedTouchesForTouch:)]) {
        touch = [[event predictedTouchesForTouch:touch] lastObject] ?: touch;
    }
    self.currentPoint = [touch locationInView:self];

    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    self.currentPoint = [[touches anyObject] locationInView:self];
    [self.previousPaths addObject:[self pathForCurrentLine]];
    self.originPoint = self.currentPoint = CGPointZero;

    [self setNeedsDisplay];
}

@end


回答2:

UIBezierPath is building a path from your instructions. Imagine a pen. When you say, "moveToPoint:" it moves the pen to that point. When you say "lineToPoint:" it puts the pen down and moves it from the current location to the new point. And so on.

To get the effect you desire, you will need to create a new path whenever the touches move, drawing a line from the original point to the current touch position.