Ser*_*nce 9 iphone cocoa-touch ios
我有一个简单的椭圆形(由CGMutablePaths组成),我希望用户能够在其周围拖动一个对象.只是想知道这样做有多复杂,我需要知道大量的数学和物理,还是有一些简单的内置方式可以让我这样做?IE用户在椭圆周围拖动此对象,并围绕它运行.
rob*_*off 46
这是一个有趣的问题.我们想拖动一个对象,但是将它限制在一个对象上CGPath.你说你有"一个简单的椭圆形",但那很无聊.让我们用图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)
鳍.我已将所有代码放在一个简单的下载中.请享用.
| 归档时间: |
|
| 查看次数: |
2480 次 |
| 最近记录: |