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

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

问题描述

我有一个简单的椭圆形(由 CGMutablePaths 组成),我希望用户能够从中拖动一个对象.只是想知道这样做有多复杂,我是否需要了解大量的数学和物理知识,或者是否有一些简单的内置方法可以让我做到这一点?IE 用户围绕椭圆拖动这个对象,并围绕它运行.

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.

推荐答案

这是一个有趣的问题.我们想要拖动一个对象,但将其限制在 CGPath 上.你说你有一个简单的椭圆形",但这很无聊.让我们用图 8 来做.完成后看起来像这样:

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:

那我们怎么做呢?给定一个任意点,在 Bezier 样条上找到最近的点是相当复杂的.让我们用蛮力做到这一点.我们将沿着路径创建一个紧密间隔的点数组.对象从这些点之一开始.当我们尝试拖动对象时,我们将查看相邻点.如果其中一个更近,我们会将对象移动到该相邻点.

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.

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

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.

这意味着我们需要遍历 CGPath 的元素.迭代 CGPath 元素的唯一方法是使用 CGPathApply 函数,它接受回调.用块遍历路径元素会更好,所以让我们向 UIBezierPath 添加一个类别.我们首先使用启用 ARC 的单一视图应用程序"模板创建一个新项目.我们添加一个类别:

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

实现非常简单.我们只是将块作为路径应用器函数的 info 参数传递.

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 {

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

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

    UIBezierPath *path_;

看到路径会很好,所以我们将使用 CAShapeLayer 来显示它.(我们需要将 QuartzCore 框架添加到我们的目标中才能使其工作.)

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

我们需要将沿路径的点数组存储在某处.让我们使用 NSMutableData:

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

    NSMutableData *pathPointsData_;

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

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

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

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

在您的问题中,您说您使用的是简单的椭圆形",但这很无聊.让我们画一个漂亮的图 8.弄清楚我在做什么留给读者作为练习:

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

为了找到点,我们需要让 Core Graphics 对路径进行划线":

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

事实证明,当 Core Graphics 对路径进行虚线处理时,它可以创建稍微重叠的段.我们希望通过过滤掉与其前任太接近的每个点来消除这些,因此我们将定义最小点间距离:

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

我们需要创建 NSMutableData 来保存 CGPoints:

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) {

每个路径元素都可以是移动到"、线到"、二次曲线到"、曲线到"(三次曲线)或关闭-小路".除了 close-path 之外,所有这些都定义了一个段端点,我们从之前的辅助函数中获取了它:

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

现在我们可以初始化我们的 pathPoints_pathPointsCount_ ivars:

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

我们加入了一个 default 子句,这样 clang 就不会警告我们我们不关心的其他状态:

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

如果手柄沿路径移动了一些偏移量,那只剩下计算手柄到点的距离的小问题了.你的老朋友 hypotf 计算了欧几里得距离,所以你不必:

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

(您可以通过使用平方距离来加快速度,以避免 hypotf 正在计算的平方根.)

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

还有一个小细节:指向点数组的索引需要在两个方向环绕.这就是我们一直依赖神秘的 handlePathPointIndexWithOffset: 方法来做的:

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天全站免登陆