将UIView围绕由CGMutablePaths组成的形状拖动
我有一个简单的椭圆形(由CGMutablePaths组成),我希望用户能够在其中拖动一个对象。 只是想知道这样做有多复杂,我是否需要知道大量的math和物理学,还是有一些简单的方法可以让我做到这一点? IE浏览器用户在椭圆周围拖动这个对象,并绕过它。
这是一个有趣的问题。 我们想要拖动一个对象,但是将它约束在一个CGPath
。 你说你有一个“简单的椭圆形”,但那很无聊。 让我们用图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应用函数的info
parameter 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一个可以容纳CGPoint
的NSMutableData
:
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
鳍。 我已经把所有的代码放在一个简单的下载中 。 请享用。