将UIView围绕由CGMutablePaths组成的形状拖动

我有一个简单的椭圆形(由CGMutablePaths组成),我希望用户能够在其中拖动一个对象。 只是想知道这样做有多复杂,我是否需要知道大量的math和物理学,还是有一些简单的方法可以让我做到这一点? IE浏览器用户在椭圆周围拖动这个对象,并绕过它。

这是一个有趣的问题。 我们想要拖动一个对象,但是将它约束在一个CGPath 。 你说你有一个“简单的椭圆形”,但那很无聊。 让我们用图8来做。当我们完成时,它会看起来像这样:

图-8-拖动

那么我们该怎么做呢? 给定一个任意点,在Bezier样条上find最接近的点是相当复杂的。 我们用蛮力来做吧。 我们将在path上紧密排列一些点。 对象从这些点之一开始。 当我们试图拖动对象时,我们将看看邻近点。 如果任何一个更近,我们将把对象移动到该邻居点。

即使沿着贝塞尔曲线获得一系列紧密间隔的点也不是微不足道的,但是有一种方法可以让Core Graphics为我们做这件事。 我们可以使用一个简短的短划线模式的CGPathCreateCopyByDashingPath 。 这创造了许多短片段的新path。 我们将把每个段的端点作为我们的点数组。

这意味着我们需要迭代CGPath的元素。 迭代CGPath元素的唯一方法是使用CGPathApply函数,该函数需要callback。 用块来迭代path元素会更好,所以让我们向UIBezierPath添加一个类别。 我们首先使用“Single View Application”模板创build一个新项目,并启用ARC。 我们添加一个类别:

 @interface UIBezierPath (forEachElement) - (void)forEachElement:(void (^)(CGPathElement const *element))block; @end 

实现非常简单。 我们只是将该块作为path应用函数的infoparameter passing。

 #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 

对于这个玩具项目,我们会在视图控制器中做所有的事情。 我们需要一些实例variables:

 @implementation ViewController { 

我们需要一个伊娃来保持对象所遵循的path。

  UIBezierPath *path_; 

看到path会很高兴,所以我们将使用CAShapeLayer来显示它。 (我们需要将QuartzCore框架添加到我们的目标中,以使其工作。)

  CAShapeLayer *pathLayer_; 

我们需要在某个地方存储path点的数组。 我们来使用一个NSMutableData

  NSMutableData *pathPointsData_; 

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

  CGPoint const *pathPoints_; 

我们需要知道有多less点:

  NSInteger pathPointsCount_; 

对于“对象”,我们将在屏幕上有一个可拖动的视图。 我把它称为“处理”:

  UIView *handleView_; 

我们需要知道手柄当前在哪个path上:

  NSInteger handlePathPointIndex_; 

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

  CGPoint desiredHandleCenter_; } 

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

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

我们创build如下的path显示层:

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

请注意,我们还没有设置path图层的path! 目前还不知道这条路是否为时尚早,因为我的观点尚未确定。

我们将画出一个红色的圆圈:

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

在视图布局时,我们需要根据视图的大小创buildpath,计算path上的点,使path层显示path,并确保path位于path上:

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

接下来,我们将要计算沿着该path的点的数组。 我们需要一个帮助程序来选取每个path元素的端点:

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

为了find要点,我们需要问核心graphics“破折号”的path:

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

事实certificate,当核心graphics破碎的path,它可以创build有些重叠的部分。 我们希望通过筛选出与其前任太接近的每个点来消除这些问题,所以我们将定义一个最小的点间距离:

  static CGFloat const kMinimumDistance = 0.1f; 

要做这个过滤,我们需要跟踪那个前辈:

  __block CGPoint priorPoint = { HUGE_VALF, HUGE_VALF }; 

我们需要创build一个可以容纳CGPointNSMutableData

  pathPointsData_ = [[NSMutableData alloc] init]; 

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

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

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

  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_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) < kMinimumDistance) { pathPointsCount_ -= 1; } } 

Blammo。 创build点数组。 噢,我们还需要更新path层。 振作起来:

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

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

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

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

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

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

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

那么我们如何把握把手呢? 我们想要沿着path滑动它。 首先,我们必须找出将其滑动的方向:

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

好的,至less有一个方向会把手柄移近。 我们来看看哪一个:

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

如果手柄沿path移动一定的偏移量,那么只需要计算从手柄到点的距离就可以了。 你的老伙计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); } 

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

另外一个小细节:点数组中的索引需要在两个方向上进行包装。 这就是我们一直依靠神秘的handlePathPointIndexWithOffset:方法来做到的:

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

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