単純な楕円形(CGMutablePathsで構成)があり、そこからユーザーがオブジェクトをドラッグできるようにします。これを行うのがどれほど複雑か疑問に思っていますが、私はたくさんの数学と物理学を知る必要がありますか、それとも私がこれを行うことを可能にするいくつかの簡単な組み込みの方法がありますか?IEユーザーは、このオブジェクトを楕円の周りにドラッグし、それを周回します。
1 に答える
これは興味深い問題です。オブジェクトをドラッグしたいのですが、オブジェクトがにあるように制約しますCGPath
。「シンプルな楕円形」とおっしゃっていましたが、つまらないです。図8でやってみましょう。完了すると次のようになります。
では、これをどのように行うのでしょうか?任意の点が与えられると、ベジェスプライン上の最も近い点を見つけることはかなり複雑です。力ずくでやってみましょう。パスに沿って間隔を空けて配置されたポイントの配列を作成します。オブジェクトは、これらのポイントの1つから始まります。オブジェクトをドラッグしようとすると、隣接するポイントが表示されます。どちらかが近い場合は、オブジェクトをその隣接ポイントに移動します。
ベジェ曲線に沿って間隔の狭いポイントの配列を取得することも簡単ではありませんが、CoreGraphicsにそれを実行させる方法があります。CGPathCreateCopyByDashingPath
短いダッシュパターンで使用できます。これにより、多くの短いセグメントを持つ新しいパスが作成されます。各セグメントの端点を点の配列として使用します。
つまり、の要素を反復処理する必要がありCGPath
ます。aの要素を反復処理する唯一の方法CGPath
はCGPathApply
、コールバックを受け取る関数を使用することです。ブロックを使用してパス要素を反復処理する方がはるかに便利なので、にカテゴリを追加しましょうUIBezierPath
。まず、ARCを有効にして、「シングルビューアプリケーション」テンプレートを使用して新しいプロジェクトを作成します。カテゴリを追加します:
@interface UIBezierPath (forEachElement)
- (void)forEachElement:(void (^)(CGPathElement const *element))block;
@end
実装は非常に簡単です。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
このおもちゃのプロジェクトでは、ViewControllerで他のすべてを行います。いくつかのインスタンス変数が必要になります。
@implementation ViewController {
オブジェクトがたどるパスを保持するためにivarが必要です。
UIBezierPath *path_;
パスが表示されると便利なので、を使用しCAShapeLayer
て表示します。QuartzCore
(これを機能させるには、ターゲットにフレームワークを追加する必要があります。)
CAShapeLayer *pathLayer_;
パスに沿ったポイントの配列をどこかに格納する必要があります。を使用してみましょうNSMutableData
:
NSMutableData *pathPointsData_;
ポインターとして入力された、ポイントの配列へのポインターが必要になりCGPoint
ます。
CGPoint const *pathPoints_;
そして、それらのポイントがいくつあるかを知る必要があります。
NSInteger pathPointsCount_;
「オブジェクト」については、画面上にドラッグ可能なビューが表示されます。私はそれを「ハンドル」と呼んでいます:
UIView *handleView_;
ハンドルが現在どのパスポイントにあるかを知る必要があります。
NSInteger handlePathPointIndex_;
また、パンジェスチャがアクティブな間、ユーザーがハンドルをドラッグしようとした場所を追跡する必要があります。
CGPoint desiredHandleCenter_;
}
今、私たちはそれらすべてのivarを初期化する作業に取り掛かる必要があります!ビューとレイヤーは次の場所で作成できますviewDidLoad
:
- (void)viewDidLoad {
[super viewDidLoad];
[self initPathLayer];
[self initHandleView];
[self initHandlePanGestureRecognizer];
}
次のようにパス表示レイヤーを作成します。
- (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_];
}
パスレイヤーのパスはまだ設定されていないことに注意してください。私のビューはまだ最終的なサイズでレイアウトされていないため、現時点でパスを知るのは時期尚早です。
ハンドルに赤い円を描きます。
- (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];
}
ビューのレイアウト時に、ビューのサイズに基づいてパスを作成し、パスに沿ったポイントを計算し、パスレイヤーにパスを表示させ、ハンドルがパス上にあることを確認する必要があります。
- (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];
}
次に、そのパスに沿ったポイントの配列を計算します。各パス要素のエンドポイントを選択するためのヘルパールーチンが必要です。
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];
}
ポイントを見つけるには、CoreGraphicsにパスを「ダッシュ」するように依頼する必要があります。
- (void)createPathPoints {
CGPathRef cgDashedPath = CGPathCreateCopyByDashingPath(path_.CGPath, NULL, 0, (CGFloat[]){ 1.0f, 1.0f }, 2);
UIBezierPath *dashedPath = [UIBezierPath bezierPathWithCGPath:cgDashedPath];
CGPathRelease(cgDashedPath);
Core Graphicsがパスをダッシュすると、わずかにオーバーラップするセグメントが作成される可能性があります。前のポイントに近すぎる各ポイントを除外することでこれらを排除したいので、最小のポイント間距離を定義します。
static CGFloat const kMinimumDistance = 0.1f;
フィルタリングを行うには、その前任者を追跡する必要があります。
__block CGPoint priorPoint = { HUGE_VALF, HUGE_VALF };
sNSMutableData
を保持するを作成する必要があります。CGPoint
pathPointsData_ = [[NSMutableData alloc] init];
ついに、破線のパスの要素を反復処理する準備が整いました。
[dashedPath forEachElement:^(const CGPathElement *element) {
各パス要素は、「move-to」、「line-to」、「quadratic-curve-to」、「curve-to」(3次曲線)、または「close-path」にすることができます。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_;
しかし、フィルタリングする必要のあるポイントがもう1つあります。パスに沿った最初のポイントが最後のポイントに近すぎる可能性があります。その場合は、カウントを減らして最後のポイントを破棄します。
if (pathPointsCount_ > 1 && hypotf(pathPoints_[0].x - priorPoint.x, pathPoints_[0].y - priorPoint.y) < kMinimumDistance) {
pathPointsCount_ -= 1;
}
}
ブラムモ。ポイント配列が作成されました。そうそう、パスレイヤーも更新する必要があります。気を引き締めてください:
- (void)layoutPathLayer {
pathLayer_.path = path_.CGPath;
pathLayer_.frame = self.view.bounds;
}
これで、ハンドルをドラッグしてパス上にとどまるようにする必要があります。パンジェスチャレコグナイザは次のアクションを送信します。
- (void)handleWasPanned:(UIPanGestureRecognizer *)recognizer {
switch (recognizer.state) {
これがパンの開始(ドラッグ)である場合は、ハンドルの開始位置を目的の位置として保存するだけです。
case UIGestureRecognizerStateBegan: {
desiredHandleCenter_ = handleView_.center;
break;
}
それ以外の場合は、ドラッグに基づいて目的の場所を更新してから、パスに沿って新しい目的の場所に向かってハンドルをスライドさせる必要があります。
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
CGPoint translation = [recognizer translationInView:self.view];
desiredHandleCenter_.x += translation.x;
desiredHandleCenter_.y += translation.y;
[self moveHandleTowardPoint:desiredHandleCenter_];
break;
}
デフォルトの句を挿入して、clangが気にしない他の状態について警告しないようにします。
default:
break;
}
最後に、ジェスチャレコグナイザーの翻訳をリセットします。
[recognizer setTranslation:CGPointZero inView:self.view];
}
では、どのようにしてハンドルをポイントに向かって動かすのでしょうか?パスに沿ってスライドさせたい。まず、スライドさせる方向を把握する必要があります。
- (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;
OK、少なくとも1つの方向でハンドルが近づきます。どれを見つけましょう:
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];
}
これにより、ハンドルがパスに沿ってオフセットで移動した場合に、ハンドルからポイントまでの距離を計算するという小さな問題が残ります。古い仲間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);
}
(計算中の平方根を回避するために、距離の2乗を使用することで、処理を高速化できますhypotf
。)
もう1つの小さな詳細:points配列へのインデックスは、両方向にラップアラウンドする必要があります。それが、私たちが神秘的な方法に頼ってきたものhandlePathPointIndexWithOffset:
です。
- (NSInteger)handlePathPointIndexWithOffset:(NSInteger)offset {
NSInteger index = handlePathPointIndex_ + offset;
while (index < 0) {
index += pathPointsCount_;
}
while (index >= pathPointsCount_) {
index -= pathPointsCount_;
}
return index;
}
@end
フィン。簡単にダウンロードできるように、すべてのコードを要点にまとめました。楽しみ。