在由CGMutablePaths组成的Shape周围拖动UIView

Ser*_*nce 9 iphone cocoa-touch ios

我有一个简单的椭圆形(由CGMutablePaths组成),我希望用户能够在其周围拖动一个对象.只是想知道这样做有多复杂,我需要知道大量的数学和物理,还是有一些简单的内置方式可以让我这样做?IE用户在椭圆周围拖动此对象,并围绕它运行.

rob*_*off 46

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

图-8-拖动

那我们该怎么做呢?给定一个任意点,在Bezier样条上找到最近点是相当复杂的.让我们通过蛮力来做.我们将沿路径制作一系列紧密间隔的点.该对象从其中一个点开始.当我们尝试拖动对象时,我们将查看相邻点.如果其中一个更近,我们将把对象移动到该邻居点.

即使沿贝塞尔曲线获得一系列紧密间隔的点也不是微不足道的,但有一种方法可以让Core Graphics为我们做到这一点.我们可以使用CGPathCreateCopyByDashingPath短划线图案.这将创建一个包含许多短段的新路径.我们将每个段的端点作为我们的点数组.

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

@interface UIBezierPath (forEachElement)

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

@end
Run Code Online (Sandbox Code Playgroud)

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

#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
Run Code Online (Sandbox Code Playgroud)

对于这个玩具项目,我们将在视图控制器中执行其他所有操作.我们需要一些实例变量:

@implementation ViewController {
Run Code Online (Sandbox Code Playgroud)

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

    UIBezierPath *path_;
Run Code Online (Sandbox Code Playgroud)

看到路径会很高兴,所以我们用a CAShapeLayer来显示它.(我们需要将QuartzCore框架添加到我们的目标中以实现此目的.)

    CAShapeLayer *pathLayer_;
Run Code Online (Sandbox Code Playgroud)

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

    NSMutableData *pathPointsData_;
Run Code Online (Sandbox Code Playgroud)

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

    CGPoint const *pathPoints_;
Run Code Online (Sandbox Code Playgroud)

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

    NSInteger pathPointsCount_;
Run Code Online (Sandbox Code Playgroud)

对于"对象",我们将在屏幕上显示可拖动的视图.我称之为"句柄":

    UIView *handleView_;
Run Code Online (Sandbox Code Playgroud)

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

    NSInteger handlePathPointIndex_;
Run Code Online (Sandbox Code Playgroud)

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

    CGPoint desiredHandleCenter_;
}
Run Code Online (Sandbox Code Playgroud)

现在我们必须开始工作,初始化所有这些ivars!我们可以在以下位置创建视图和图层viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initPathLayer];
    [self initHandleView];
    [self initHandlePanGestureRecognizer];
}
Run Code Online (Sandbox Code Playgroud)

我们创建路径显示层,如下所示:

- (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_];
}
Run Code Online (Sandbox Code Playgroud)

请注意,我们还没有设置路径图层的路径!现在知道这条路还为时过早,因为我的观点还没有按照最终的规模进行布局.

我们将为句柄绘制一个红色圆圈:

- (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_];
}
Run Code Online (Sandbox Code Playgroud)

同样,现在要确切知道我们需要将把手放在屏幕上的时间还为时尚早.我们将在视图布局时间处理这个问题.

我们还需要在手柄上附加一个平移手势识别器:

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

在视图布局时,我们需要根据视图的大小创建路径,计算沿路径的点,使路径图层显示路径,并确保句柄在路径上:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    [self createPath];
    [self createPathPoints];
    [self layoutPathLayer];
    [self layoutHandleView];
}
Run Code Online (Sandbox Code Playgroud)

在你的问题中,你说你使用的是"简单的椭圆形",但这很无聊.让我们画出一个很好的数字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 clockwise:NO];
    [path_ addArcWithCenter:bottomCenter radius:radius startAngle:-M_PI_4 endAngle:M_PI + M_PI_4 clockwise:YES];
    [path_ closePath];
}
Run Code Online (Sandbox Code Playgroud)

接下来,我们将要计算沿该路径的点阵列.我们需要一个辅助例程来挑选每个路径元素的端点:

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];
}
Run Code Online (Sandbox Code Playgroud)

为了找到这些点,我们需要让Core Graphics"破解"这条路径:

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

事实证明,当Core Graphics破坏路径时,它可以创建略微重叠的段.我们希望通过过滤掉与其前身太接近的每个点来消除它们,因此我们将定义最小的点间距离:

    static CGFloat const kMinimumDistance = 0.1f;
Run Code Online (Sandbox Code Playgroud)

要进行过滤,我们需要跟踪前一个过滤器:

    __block CGPoint priorPoint = { HUGE_VALF, HUGE_VALF };
Run Code Online (Sandbox Code Playgroud)

我们需要创建NSMutableData将持有CGPoints的:

    pathPointsData_ = [[NSMutableData alloc] init];
Run Code Online (Sandbox Code Playgroud)

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

    [dashedPath forEachElement:^(const CGPathElement *element) {
Run Code Online (Sandbox Code Playgroud)

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

        CGPoint *p = lastPointOfPathElement(element);
        if (!p)
            return;
Run Code Online (Sandbox Code Playgroud)

如果端点太接近前一点,我们将其丢弃:

        if (hypotf(p->x - priorPoint.x, p->y - priorPoint.y) < kMinimumDistance)
            return;
Run Code Online (Sandbox Code Playgroud)

否则,我们将其附加到数据并将其保存为下一个端点的前任:

        [pathPointsData_ appendBytes:p length:sizeof *p];
        priorPoint = *p;
    }];
Run Code Online (Sandbox Code Playgroud)

现在我们可以初始化我们的pathPoints_和pathPointsCount_ivars:

    pathPoints_ = (CGPoint const *)pathPointsData_.bytes;
    pathPointsCount_ = pathPointsData_.length / sizeof *pathPoints_;
Run Code Online (Sandbox Code Playgroud)

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

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

Blammo.创建点阵列.哦,是的,我们还需要更新路径图层.振作起来:

- (void)layoutPathLayer {
    pathLayer_.path = path_.CGPath;
    pathLayer_.frame = self.view.bounds;
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以担心拖动手柄并确保它保持在路径上.平移手势识别器发送此操作:

- (void)handleWasPanned:(UIPanGestureRecognizer *)recognizer {
    switch (recognizer.state) {
Run Code Online (Sandbox Code Playgroud)

如果这是平移(拖动)的开始,我们只想将句柄的起始位置保存为其所需位置:

        case UIGestureRecognizerStateBegan: {
            desiredHandleCenter_ = handleView_.center;
            break;
        }
Run Code Online (Sandbox Code Playgroud)

否则,我们需要根据拖动更新所需位置,然后沿着路径将手柄滑向新的所需位置:

        case UIGestureRecognizerStateChanged:
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            CGPoint translation = [recognizer translationInView:self.view];
            desiredHandleCenter_.x += translation.x;
            desiredHandleCenter_.y += translation.y;
            [self moveHandleTowardPoint:desiredHandleCenter_];
            break;
        }
Run Code Online (Sandbox Code Playgroud)

我们提出了一个默认条款,因此clang不会警告我们关于我们不关心的其他状态:

        default:
            break;
    }
Run Code Online (Sandbox Code Playgroud)

最后,我们重置了手势识别器的翻译:

    [recognizer setTranslation:CGPointZero inView:self.view];
}
Run Code Online (Sandbox Code Playgroud)

那么我们如何将手柄移向一个点呢?我们想沿着路径滑动它.首先,我们必须找出滑动的方向:

- (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];
Run Code Online (Sandbox Code Playgroud)

两个方向都可能会使手柄从所需的点进一步移动,所以在这种情况下让我们挽救:

    if (currentDistance <= earlierDistance && currentDistance <= laterDistance)
        return;
Run Code Online (Sandbox Code Playgroud)

好的,所以至少有一个方向会使把手移近.让我们弄清楚哪一个:

    NSInteger direction;
    CGFloat distance;
    if (earlierDistance < laterDistance) {
        direction = -1;
        distance = earlierDistance;
    } else {
        direction = 1;
        distance = laterDistance;
    }
Run Code Online (Sandbox Code Playgroud)

但是我们只检查了手柄起点的最近邻居.我们希望尽可能沿着该方向沿路径滑动,只要手柄越来越接近所需的点:

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

最后,将句柄的位置更新为我们新发现的点:

    handlePathPointIndex_ += offset;
    [self layoutHandleView];
}
Run Code Online (Sandbox Code Playgroud)

如果手柄沿着路径移动了一些偏移量,那么这就留下了计算从手柄到一个点的距离的小问题.你的老朋友hypotf计算欧几里德距离,所以你不必:

- (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);
}
Run Code Online (Sandbox Code Playgroud)

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

还有一个细节:点数组的索引需要在两个方向上包围.这就是我们一直依赖的神秘handlePathPointIndexWithOffset:方法:

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

@end
Run Code Online (Sandbox Code Playgroud)

鳍.我已将所有代码放在一个简单的下载中.请享用.

  • 罗布 - 非常感谢你花时间这么做.不能够感谢你.你是iPhone开发之神!::崇拜:: (4认同)