Created
December 2, 2012 06:51
-
-
Save mayoff/4187399 to your computer and use it in GitHub Desktop.
dragging an object along a CGPath on iOS demo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <UIKit/UIKit.h> | |
@interface UIBezierPath (forEachElement) | |
- (void)forEachElement:(void (^)(CGPathElement const *element))block; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "UIBezierPath+forEachElement.h" | |
typedef void (^UIBezierPath_forEachElement_Block)(CGPathElement const *element); | |
@implementation UIBezierPath (forEachElement) | |
static void applyBlockToPathElement(void *info, CGPathElement const *element) { | |
__unsafe_unretained UIBezierPath_forEachElement_Block block =(__bridge UIBezierPath_forEachElement_Block)info; | |
block(element); | |
} | |
- (void)forEachElement:(void (^)(const CGPathElement *))block { | |
CGPathApply(self.CGPath, (__bridge void *)block, applyBlockToPathElement); | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This code is described here: | |
// http://stackoverflow.com/questions/13664615/drag-uiview-around-oval-shape-comprised-of-cgmutablepaths | |
#import "ViewController.h" | |
#import "UIBezierPath+forEachElement.h" | |
@interface ViewController () | |
@end | |
@implementation ViewController { | |
UIBezierPath *path_; | |
CAShapeLayer *pathLayer_; | |
NSMutableData *pathPointsData_; | |
CGPoint const *pathPoints_; | |
NSInteger pathPointsCount_; | |
UIView *handleView_; | |
NSInteger handlePathPointIndex_; | |
CGPoint desiredHandleCenter_; | |
} | |
- (void)viewDidLoad { | |
[super viewDidLoad]; | |
[self initPathLayer]; | |
[self initHandleView]; | |
[self initHandlePanGestureRecognizer]; | |
} | |
- (void)initPathLayer { | |
pathLayer_ = [CAShapeLayer layer]; | |
pathLayer_.lineWidth = 1; | |
pathLayer_.fillColor = nil; | |
pathLayer_.strokeColor = [UIColor blackColor].CGColor; | |
pathLayer_.lineCap = kCALineCapButt; | |
pathLayer_.lineJoin = kCALineJoinRound; | |
[self.view.layer addSublayer:pathLayer_]; | |
} | |
- (void)initHandleView { | |
handlePathPointIndex_ = 0; | |
CGRect rect = CGRectMake(0, 0, 30, 30); | |
CAShapeLayer *circleLayer = [CAShapeLayer layer]; | |
circleLayer.fillColor = nil; | |
circleLayer.strokeColor = [UIColor redColor].CGColor; | |
circleLayer.lineWidth = 2; | |
circleLayer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(rect, circleLayer.lineWidth, circleLayer.lineWidth)].CGPath; | |
circleLayer.frame = rect; | |
handleView_ = [[UIView alloc] initWithFrame:rect]; | |
[handleView_.layer addSublayer:circleLayer]; | |
[self.view addSubview:handleView_]; | |
} | |
- (void)initHandlePanGestureRecognizer { | |
UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleWasPanned:)]; | |
[handleView_ addGestureRecognizer:recognizer]; | |
} | |
- (void)viewDidLayoutSubviews { | |
[super viewDidLayoutSubviews]; | |
[self createPath]; | |
[self createPathPoints]; | |
[self layoutPathLayer]; | |
[self layoutHandleView]; | |
} | |
- (void)layoutHandleView { | |
// Make sure the index is in bounds. | |
handlePathPointIndex_ = [self handlePathPointIndexWithOffset:0]; | |
handleView_.center = [pathLayer_ convertPoint:pathPoints_[handlePathPointIndex_] toLayer:self.view.layer]; | |
} | |
- (void)createPath { | |
CGRect bounds = self.view.bounds; | |
CGFloat const radius = bounds.size.height / 6; | |
CGFloat const offset = 2 * radius * M_SQRT1_2; | |
CGPoint const topCenter = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds) - offset); | |
CGPoint const bottomCenter = { topCenter.x, CGRectGetMidY(bounds) + offset }; | |
path_ = [UIBezierPath bezierPath]; | |
[path_ addArcWithCenter:topCenter radius:radius startAngle:M_PI_4 endAngle:-M_PI - M_PI_4 clockwise:NO]; | |
[path_ addArcWithCenter:bottomCenter radius:radius startAngle:-M_PI_4 endAngle:M_PI + M_PI_4 clockwise:YES]; | |
[path_ closePath]; | |
} | |
static CGPoint *lastPointOfPathElement(CGPathElement const *element) { | |
int index; | |
switch (element->type) { | |
case kCGPathElementMoveToPoint: index = 0; break; | |
case kCGPathElementAddCurveToPoint: index = 2; break; | |
case kCGPathElementAddLineToPoint: index = 0; break; | |
case kCGPathElementAddQuadCurveToPoint: index = 1; break; | |
case kCGPathElementCloseSubpath: index = NSNotFound; break; | |
} | |
return index == NSNotFound ? 0 : &element->points[index]; | |
} | |
- (void)createPathPoints { | |
CGPathRef cgDashedPath = CGPathCreateCopyByDashingPath(path_.CGPath, NULL, 0, (CGFloat[]){ 1.0f, 1.0f }, 2); | |
UIBezierPath *dashedPath = [UIBezierPath bezierPathWithCGPath:cgDashedPath]; | |
CGPathRelease(cgDashedPath); | |
static CGFloat const kMinimumDistance = 0.1f; | |
__block CGPoint priorPoint = { HUGE_VALF, HUGE_VALF }; | |
pathPointsData_ = [[NSMutableData alloc] init]; | |
[dashedPath forEachElement:^(const CGPathElement *element) { | |
CGPoint *p = lastPointOfPathElement(element); | |
if (!p) | |
return; | |
if (hypotf(p->x - priorPoint.x, p->y - priorPoint.y) < kMinimumDistance) | |
return; | |
[pathPointsData_ appendBytes:p length:sizeof *p]; | |
priorPoint = *p; | |
}]; | |
pathPoints_ = (CGPoint const *)pathPointsData_.bytes; | |
pathPointsCount_ = pathPointsData_.length / sizeof *pathPoints_; | |
if (pathPointsCount_ > 1 && hypotf(pathPoints_[0].x - priorPoint.x, pathPoints_[0].y - priorPoint.y) < kMinimumDistance) { | |
pathPointsCount_ -= 1; | |
} | |
} | |
- (void)layoutPathLayer { | |
pathLayer_.path = path_.CGPath; | |
pathLayer_.frame = self.view.bounds; | |
} | |
- (void)handleWasPanned:(UIPanGestureRecognizer *)recognizer { | |
switch (recognizer.state) { | |
case UIGestureRecognizerStateBegan: { | |
desiredHandleCenter_ = handleView_.center; | |
break; | |
} | |
case UIGestureRecognizerStateChanged: | |
case UIGestureRecognizerStateEnded: | |
case UIGestureRecognizerStateCancelled: { | |
CGPoint translation = [recognizer translationInView:self.view]; | |
desiredHandleCenter_.x += translation.x; | |
desiredHandleCenter_.y += translation.y; | |
[self moveHandleTowardPoint:desiredHandleCenter_]; | |
break; | |
} | |
default: | |
break; | |
} | |
[recognizer setTranslation:CGPointZero inView:self.view]; | |
} | |
- (void)moveHandleTowardPoint:(CGPoint)point { | |
CGFloat earlierDistance = [self distanceToPoint:point ifHandleMovesByOffset:-1]; | |
CGFloat currentDistance = [self distanceToPoint:point ifHandleMovesByOffset:0]; | |
CGFloat laterDistance = [self distanceToPoint:point ifHandleMovesByOffset:1]; | |
if (currentDistance <= earlierDistance && currentDistance <= laterDistance) | |
return; | |
NSInteger direction; | |
CGFloat distance; | |
if (earlierDistance < laterDistance) { | |
direction = -1; | |
distance = earlierDistance; | |
} else { | |
direction = 1; | |
distance = laterDistance; | |
} | |
NSInteger offset = direction; | |
while (true) { | |
NSInteger nextOffset = offset + direction; | |
CGFloat nextDistance = [self distanceToPoint:point ifHandleMovesByOffset:nextOffset]; | |
if (nextDistance >= distance) | |
break; | |
distance = nextDistance; | |
offset = nextOffset; | |
} | |
handlePathPointIndex_ += offset; | |
[self layoutHandleView]; | |
} | |
- (CGFloat)distanceToPoint:(CGPoint)point ifHandleMovesByOffset:(NSInteger)offset { | |
int index = [self handlePathPointIndexWithOffset:offset]; | |
CGPoint proposedHandlePoint = pathPoints_[index]; | |
return hypotf(point.x - proposedHandlePoint.x, point.y - proposedHandlePoint.y); | |
} | |
- (NSInteger)handlePathPointIndexWithOffset:(NSInteger)offset { | |
NSInteger index = handlePathPointIndex_ + offset; | |
while (index < 0) { | |
index += pathPointsCount_; | |
} | |
while (index >= pathPointsCount_) { | |
index -= pathPointsCount_; | |
} | |
return index; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Any chance of a Swift update for this?