在由CGMutablePaths组成的Shape上拖动UIView [英] Drag UIView around Shape Comprised of CGMutablePaths

查看:164
本文介绍了在由CGMutablePaths组成的Shape上拖动UIView的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个简单的椭圆形(由CGMutablePaths组成),我希望用户能够从中拖动一个对象。只是想知道这是多么复杂,我需要知道一吨数学和物理,或者有一些简单的内置方式,将允许我这样做?

解决方案

这是一个有趣的问题。我们要拖动一个对象,但是约束它放在 CGPath 上。你说你有一个简单的椭圆形,但这是无聊。让我们用一个数字8来做。完成后,它会是这样:





那么我们该怎么做呢?给定任意点,找到贝塞尔曲线上最近的点是相当复杂的。让我们用暴力来做。我们只需要做一个沿着路径紧密排列的点的数组。对象从这些点之一开始。当我们尝试拖动对象时,我们将查看邻近点。



即使得到一个沿着Bezier曲线的紧密间隔点的数组也不是微不足道的,但是是一种获得Core Graphics为我们做的方法。我们可以使用带有短划线模式的 CGPathCreateCopyByDashingPath 。这将创建具有许多短片段的新路径。我们将每个段的端点作为点数组。



这意味着我们需要遍历 CGPath 。对 CGPath 的元素进行迭代的唯一方法是使用 CGPathApply 函数,该函数接受回调。使用块来遍历路径元素会更好一些,所以让我们在 UIBezierPath 中添加一个类别。我们首先使用Single View Application模板创建一个新项目,启用ARC。我们添加一个类别:

  @interface UIBezierPath(forEachElement)

- (void)forEachElement :( void(^)(CGPathElement const * element))block;

@end

实现非常简单。我们只是将块作为路径applier函数的信息参数传递。

  #importUIBezierPath + 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

将在视图控制器中做其他一切。我们需要一些实例变量:

  @implementation ViewController {

我们需要一个ivar来保存对象所遵循的路径。

  UIBezierPath * path_; 

这将是很高兴看到的路径,所以我们将使用 CAShapeLayer 以显示它。 (我们需要将 QuartzCore 框架添加到我们的目标中。)

  CAShapeLayer * pathLayer_; 

我们需要存储点的数组。让我们使用 NSMutableData

  NSMutableData * pathPointsData_; 

我们需要一个指向数组数组的指针,类型为 CGPoint 指针:

  CGPoint const * pathPoints_; 

我们需要知道这些点有多少:

  NSInteger pathPointsCount_; 

对于对象,我们将在屏幕上有一个可拖动的视图。我叫它handle:

  UIView * handleView_; 

我们需要知道句柄当前所在的路径点:

  NSInteger handlePathPointIndex_; 

当平移手势处于活动状态时,我们需要跟踪用户尝试拖动的位置

  CGPoint desiredHandleCenter_; 
}



现在我们要开始初始化所有这些ivars了!我们可以在 viewDidLoad 中创建视图和图层:

   - )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_];
}

注意,我们还没有设置路径图层的路径!现在还不知道这个时候的路径,因为我的观点还没有按照它的最终尺寸排列。



我们将绘制一个红色圆圈handle:

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



在你的问题中,你说你使用的是简单的椭圆形 ,但这是无聊。让我们画一个好的图8.把我所做的工作留给读者做一个练习:

   - (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顺时针:NO];
[path_addArcWithCenter:bottomCenter radius:radius startAngle:-M_PI_4 endAngle:M_PI + M_PI_4顺时针:YES];
[path_ closePath];
}

接下来,我们要计算沿着该路径的点数组。我们需要一个帮助程序来选择每个路径元素的终点:

  static CGPoint * lastPointOfPathElement(CGPathElement const * element ){
int index;
switch(element-> type){
case kCGPathElementMoveToPoint:index = 0;打破;
case KCGPathElementAddCurveToPoint:index = 2;打破;
case kCGPathElementAddLineToPoint:index = 0;打破;
case kCGPathElementAddQuadCurveToPoint:index = 1;打破;
case kCGPathElementCloseSubpath:index = NSNotFound;打破;
}
return index == NSNotFound? 0:& element-> points [index];
}

要找到点,我们需要让Core Graphics路径:

   - (void)createPathPoints {
CGPathRef cgDashedPath = CGPathCreateCopyByDashingPath(path_.CGPath,NULL,0, CGFloat []){1.0f,1.0f},2);
UIBezierPath * dottedPath = [UIBezierPath bezierPathWithCGPath:cgDashedPath];
CGPathRelease(cgDashedPath);

事实证明,当Core Graphics对路径进行划线时,它可以创建轻微重叠的线段。我们将通过过滤掉与前辈太近的每个点来消除这些点,因此我们将定义一个最小点间距离:

  static CGFloat const kMinimumDistance = 0.1f; 

要进行过滤,我们需要跟踪该前辈:

  __block CGPoint priorPoint = {HUGE_VALF,HUGE_VALF}; 

我们需要创建 NSMutableData 保持 CGPoint s:

  pathPointsData_ = [[NSMutableData alloc] init ]; 

最后,我们准备迭代虚线路径的元素:

  [brokenPath forEachElement:^(const CGPathElement * element){

每个路径元素可以是移动到,线到,二次曲线到,曲线到(其是三次曲线)或近路。除了close-path之外的所有这些都定义了一个段端点,我们从之前的帮助函数中取出:

  CGPoint * p = lastPointOfPathElement(element); 
if(!p)
return;

如果端点离前一点太近,我们将其舍弃:

  if(hypotf(p-> x-priorPoint.x,p-> y- priorPoint.y)< kMinimumDistance)
返回;

否则,我们将它附加到数据并保存为下一个端点的前身: p>

  [pathPointsData_ appendBytes:p length:sizeof * p]; 
priorPoint = * p;
}];



现在我们可以初始化 pathPoints _ code> pathPointsCount _
ivars:

  pathPoints_ =(CGPoint const *)pathPointsData_.bytes; 
pathPointsCount_ = pathPointsData_.length / sizeof * pathPoints_;

但我们还有一点需要过滤。沿着路径的第一个点可能太接近最后一个点。如果是,我们将通过递减计数来丢弃最后一个点:

  if(pathPointsCount_> 1&& hypotf(pathPoints_ [0] .x  -  priorPoint.x,pathPoints_ [0] .y-priorPoint.y) pathPointsCount_  -  = 1; 
}
}

Blammo。创建点阵列。哦,是的,我们还需要更新路径层。支持自己:

   - (void)layoutPathLayer {
pathLayer_.path = path_.CGPath;
pathLayer_.frame = self.view.bounds;
}

现在我们可以担心拖动手柄,路径。平移手势识别器发送这个动作:

   - (void)handleWasPanned :( UIPanGestureRecognizer *)识别器{
switch如果这是平移(拖动)的开始,我们只想要一个保存句柄的起始位置为其所需位置:

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

我们放入一个默认子句,clang不会警告我们另一个说明我们不在乎:

 默认值:
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];

这两个方向可能会将手柄从所需的位置进一步移动,所以让我们放弃case:

  if(currentDistance< = earlyDistance&& currentDistance< = laterDistance)
return;

确定,所以至少有一个方向会移动手柄更近。让我们找出哪一个:

  NSInteger方向; 
CGFloat距离;
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];
}

这只是计算从句柄到点的距离,如果手柄沿着路径移动一些偏移量。您的旧伙伴 hypotf 计算欧氏距离,因此您不必:

   - (CGFloat)distanceToPoint:(CGPoint)point ifHandleMovesByOffset:(NSInteger)offset {
int index = [self handlePathPointIndexWithOffset:offset];
CGPoint suggestedHandlePoint = pathPoints_ [index];
return hypotf(point.x - proposedHandlePoint.x,point.y - proposedHandlePoint.y);
}

(你可以使用平方距离来加速, hypotf 是计算。)



另一个细节:方向。这是我们一直依靠神秘的 handlePathPointIndexWithOffset:方法来做:

   - (NSInteger)handlePathPointIndexWithOffset:(NSInteger)offset {
NSInteger index = handlePathPointIndex_ + offset;
while(index< 0){
index + = pathPointsCount_;
}
while(index> = pathPointsCount_){
index - = pathPointsCount_;
}
return index;
}

@end

我已将所有代码放在一个提示中,以方便下载。享受。


I have a simple oval shape (comprised of CGMutablePaths) from which I'd like the user to be able to drag an object around it. Just wondering how complicated it is to do this, do I need to know a ton of math and physics, or is there some simple built in way that will allow me to do this? IE the user drags this object around the oval, and it orbits it.

解决方案

This is an interesting problem. We want to drag an object, but constrain it to lie on a CGPath. You said you have "a simple oval shape", but that's boring. Let's do it with a figure 8. It'll look like this when we're done:

So how do we do this? Given an arbitrary point, finding the nearest point on a Bezier spline is rather complicated. Let's do it by brute force. We'll just make an array of points closely spaced along the path. The object starts out on one of those points. As we try to drag the object, we'll look at the neighboring points. If either is nearer, we'll move the object to that neighbor point.

Even getting an array of closely-spaced points along a Bezier curve is not trivial, but there is a way to get Core Graphics to do it for us. We can use CGPathCreateCopyByDashingPath with a short dash pattern. This creates a new path with many short segments. We'll take the endpoints of each segment as our array of points.

That means we need to iterate over the elements of a CGPath. The only way to iterate over the elements of a CGPath is with the CGPathApply function, which takes a callback. It would be much nicer to iterate over path elements with a block, so let's add a category to UIBezierPath. We start by creating a new project using the "Single View Application" template, with ARC enabled. We add a category:

@interface UIBezierPath (forEachElement)

- (void)forEachElement:(void (^)(CGPathElement const *element))block;

@end

The implementation is very simple. We just pass the block as the info argument of the path applier function.

#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

For this toy project, we'll do everything else in the view controller. We'll need some instance variables:

@implementation ViewController {

We need an ivar to hold the path that the object follows.

    UIBezierPath *path_;

It would be nice to see the path, so we'll use a CAShapeLayer to display it. (We need to add the QuartzCore framework to our target for this to work.)

    CAShapeLayer *pathLayer_;

We'll need to store the array of points-along-the-path somewhere. Let's use an NSMutableData:

    NSMutableData *pathPointsData_;

We'll want a pointer to the array of points, typed as a CGPoint pointer:

    CGPoint const *pathPoints_;

And we need to know how many of those points there are:

    NSInteger pathPointsCount_;

For the "object", we'll have a draggable view on the screen. I'm calling it the "handle":

    UIView *handleView_;

We need to know which of the path points the handle is currently on:

    NSInteger handlePathPointIndex_;

And while the pan gesture is active, we need to keep track of where the user has tried to drag the handle:

    CGPoint desiredHandleCenter_;
}

Now we have to get to work initializing all those ivars! We can create our views and layers in viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initPathLayer];
    [self initHandleView];
    [self initHandlePanGestureRecognizer];
}

We create the path-displaying layer like this:

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

Note that we haven't set the path layer's path yet! It's too soon to know the path at this time, because my view hasn't been laid out at its final size yet.

We'll draw a red circle for the handle:

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

Again, it's too soon to know exactly where we'll need to put the handle on screen. We'll take care of that at view layout time.

We also need to attach a pan gesture recognizer to the handle:

- (void)initHandlePanGestureRecognizer {
    UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleWasPanned:)];
    [handleView_ addGestureRecognizer:recognizer];
}

At view layout time, we need to create the path based on the size of the view, compute the points along the path, make the path layer show the path, and make sure the handle is on the path:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    [self createPath];
    [self createPathPoints];
    [self layoutPathLayer];
    [self layoutHandleView];
}

In your question, you said you're using a "simple oval shape", but that's boring. Let's draw a nice figure 8. Figuring out what I'm doing is left as an exercise for the reader:

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

Next we're going to want to compute the array of points along that path. We'll need a helper routine to pick out the endpoint of each path element:

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

To find the points, we need to ask Core Graphics to "dash" the path:

- (void)createPathPoints {
    CGPathRef cgDashedPath = CGPathCreateCopyByDashingPath(path_.CGPath, NULL, 0, (CGFloat[]){ 1.0f, 1.0f }, 2);
    UIBezierPath *dashedPath = [UIBezierPath bezierPathWithCGPath:cgDashedPath];
    CGPathRelease(cgDashedPath);

It turns out that when Core Graphics dashes the path, it can create segments that slightly overlap. We'll want to eliminate those by filtering out each point that's too close to its predecessor, so we'll define a minimum inter-point distance:

    static CGFloat const kMinimumDistance = 0.1f;

To do the filtering, we'll need to keep track of that predecessor:

    __block CGPoint priorPoint = { HUGE_VALF, HUGE_VALF };

We need to create the NSMutableData that will hold the CGPoints:

    pathPointsData_ = [[NSMutableData alloc] init];

At last we're ready to iterate over the elements of the dashed path:

    [dashedPath forEachElement:^(const CGPathElement *element) {

Each path element can be a "move-to", a "line-to", a "quadratic-curve-to", a "curve-to" (which is a cubic curve), or a "close-path". All of those except close-path define a segment endpoint, which we pick up with our helper function from earlier:

        CGPoint *p = lastPointOfPathElement(element);
        if (!p)
            return;

If the endpoint is too close to the prior point, we discard it:

        if (hypotf(p->x - priorPoint.x, p->y - priorPoint.y) < kMinimumDistance)
            return;

Otherwise, we append it to the data and save it as the predecessor of the next endpoint:

        [pathPointsData_ appendBytes:p length:sizeof *p];
        priorPoint = *p;
    }];

Now we can initialize our pathPoints_ and pathPointsCount_ ivars:

    pathPoints_ = (CGPoint const *)pathPointsData_.bytes;
    pathPointsCount_ = pathPointsData_.length / sizeof *pathPoints_;

But we have one more point we need to filter. The very first point along the path might be too close to the very last point. If so, we'll just discard the last point by decrementing the count:

    if (pathPointsCount_ > 1 && hypotf(pathPoints_[0].x - priorPoint.x, pathPoints_[0].y - priorPoint.y) < kMinimumDistance) {
        pathPointsCount_ -= 1;
    }
}

Blammo. Point array created. Oh yeah, we also need to update the path layer. Brace yourself:

- (void)layoutPathLayer {
    pathLayer_.path = path_.CGPath;
    pathLayer_.frame = self.view.bounds;
}

Now we can worry about dragging the handle around and making sure it stays on the path. The pan gesture recognizer sends this action:

- (void)handleWasPanned:(UIPanGestureRecognizer *)recognizer {
    switch (recognizer.state) {

If this is the start of the pan (drag), we just want to save the starting location of the handle as its desired location:

        case UIGestureRecognizerStateBegan: {
            desiredHandleCenter_ = handleView_.center;
            break;
        }

Otherwise, we need to update the desired location based on the drag, and then slide the handle along the path toward the new desired location:

        case UIGestureRecognizerStateChanged:
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            CGPoint translation = [recognizer translationInView:self.view];
            desiredHandleCenter_.x += translation.x;
            desiredHandleCenter_.y += translation.y;
            [self moveHandleTowardPoint:desiredHandleCenter_];
            break;
        }

We put in a default clause so clang won't warn us about the other states that we don't care about:

        default:
            break;
    }

Finally we reset the translation of the gesture recognizer:

    [recognizer setTranslation:CGPointZero inView:self.view];
}

So how do we move the handle toward a point? We want to slide it along the path. First, we have to figure out which direction to slide it:

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

It's possible that both directions would move the handle further from the desired point, so let's bail out in that case:

    if (currentDistance <= earlierDistance && currentDistance <= laterDistance)
        return;

OK, so at least one of the directions will move the handle closer. Let's figure out which one:

    NSInteger direction;
    CGFloat distance;
    if (earlierDistance < laterDistance) {
        direction = -1;
        distance = earlierDistance;
    } else {
        direction = 1;
        distance = laterDistance;
    }

But we've only checked the nearest neighbors of the handle's starting point. We want to slide as far as we can along the path in that direction, as long as the handle is getting closer to the desired point:

    NSInteger offset = direction;
    while (true) {
        NSInteger nextOffset = offset + direction;
        CGFloat nextDistance = [self distanceToPoint:point ifHandleMovesByOffset:nextOffset];
        if (nextDistance >= distance)
            break;
        distance = nextDistance;
        offset = nextOffset;
    }

Finally, update the handle's position to our newly-discovered point:

    handlePathPointIndex_ += offset;
    [self layoutHandleView];
}

That just leaves the small matter of computing the distance from the handle to a point, should the handle be moved along the path by some offset. Your old buddy hypotf computes the Euclidean distance so you don't have to:

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

(You could speed things up by using squared distances to avoid the square roots that hypotf is computing.)

One more tiny detail: the index into the points array needs to wrap around in both directions. That's what we've been relying on the mysterious handlePathPointIndexWithOffset: method to do:

- (NSInteger)handlePathPointIndexWithOffset:(NSInteger)offset {
    NSInteger index = handlePathPointIndex_ + offset;
    while (index < 0) {
        index += pathPointsCount_;
    }
    while (index >= pathPointsCount_) {
        index -= pathPointsCount_;
    }
    return index;
}

@end

Fin. I've put all of the code in a gist for easy downloading. Enjoy.

这篇关于在由CGMutablePaths组成的Shape上拖动UIView的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆