4

CAShapeLayer とマスキングを使用して円形アニメーションを作成しました。これが私のコードです:

- (void) maskAnimation{


    animationCompletionBlock theBlock;
    imageView.hidden = FALSE;//Show the image view

    CAShapeLayer *maskLayer = [CAShapeLayer layer];

    CGFloat maskHeight = imageView.layer.bounds.size.height;
    CGFloat maskWidth = imageView.layer.bounds.size.width;


    CGPoint centerPoint;
    centerPoint = CGPointMake( maskWidth/2, maskHeight/2);

    //Make the radius of our arc large enough to reach into the corners of the image view.
    CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;

    //Don't fill the path, but stroke it in black.
    maskLayer.fillColor = [[UIColor clearColor] CGColor];
    maskLayer.strokeColor = [[UIColor blackColor] CGColor];

    maskLayer.lineWidth = 60;

    CGMutablePathRef arcPath = CGPathCreateMutable();

    //Move to the starting point of the arc so there is no initial line connecting to the arc
    CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);

    //Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
    CGPathAddArc(arcPath,
                 nil,
                 centerPoint.x,
                 centerPoint.y,
                 radius/2,
                 3*M_PI/2,
                 -M_PI/2,
                 NO);



    maskLayer.path = arcPath;//[aPath CGPath];//arcPath;

    //Start with an empty mask path (draw 0% of the arc)
    maskLayer.strokeEnd = 0.0;


    CFRelease(arcPath);

    //Install the mask layer into out image view's layer.
    imageView.layer.mask = maskLayer;

    //Set our mask layer's frame to the parent layer's bounds.
    imageView.layer.mask.frame = imageView.layer.bounds;

    //Create an animation that increases the stroke length to 1, then reverses it back to zero.
    CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    swipe.duration = 5;
    swipe.delegate = self;
    [swipe setValue: theBlock forKey: kAnimationCompletionBlock];
    swipe.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    swipe.fillMode = kCAFillModeForwards;
    swipe.removedOnCompletion = NO;
    swipe.autoreverses = YES;
    swipe.toValue = [NSNumber numberWithFloat: 1.0];

    [maskLayer addAnimation: swipe forKey: @"strokeEnd"];


}

これは私の背景画像です: ここに画像の説明を入力

アニメーションを実行すると、次のようになります。 ここに画像の説明を入力

しかし、私が欲しいのは、矢印の頭にこれを追加する方法がありませんか? ここに画像の説明を入力

4

3 に答える 3

17

私のもう 1 つの回答 (2 レベルのマスクのアニメーション化)にはグラフィックスの不具合があるため、アニメーションのすべてのフレームでパスを再描画することにしました。最初CALayerに のようなサブクラスを書きましょうCAShapeLayerが、矢印を描画するだけです。もともと のサブクラスにしようとしたのですCAShapeLayerが、Core Animation で適切にアニメーション化することができませんでした。

とにかく、実装するインターフェイスは次のとおりです。

@interface ArrowLayer : CALayer

@property (nonatomic) CGFloat thickness;
@property (nonatomic) CGFloat startRadians;
@property (nonatomic) CGFloat lengthRadians;
@property (nonatomic) CGFloat headLengthRadians;

@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic) CGFloat lineWidth;
@property (nonatomic) CGLineJoin lineJoin;

@end

The startRadians property is the position (in radians) of the end of the tail. The lengthRadians is the length (in radians) from the end of the tail to the tip of the arrowhead. The headLengthRadians is the length (in radians) of the arrowhead.

のプロパティの一部も再現しますCAShapeLayerlineCap常に閉じたパスを描画するため、プロパティは必要ありません。

では、このクレイジーなことをどのように実装するのでしょうか? たまたま、CALayerサブクラス で定義したい古いプロパティをすべて保存します。最初に、プロパティの合成について心配しないようにコンパイラに指示します。

@implementation ArrowLayer

@dynamic thickness;
@dynamic startRadians;
@dynamic lengthRadians;
@dynamic headLengthRadians;
@dynamic fillColor;
@dynamic strokeColor;
@dynamic lineWidth;
@dynamic lineJoin;

ただし、これらのプロパティのいずれかが変更された場合は、レイヤーを再描画する必要があることを Core Animation に伝える必要があります。そのためには、プロパティ名のリストが必要です。Objective-C ランタイムを使用してリストを取得するので、プロパティ名を再入力する必要はありません。ファイルの先頭にある必要があり#import <objc/runtime.h>、次のようにリストを取得できます。

+ (NSSet *)customPropertyKeys {
    static NSMutableSet *set;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        unsigned int count;
        objc_property_t *properties = class_copyPropertyList(self, &count);
        set = [[NSMutableSet alloc] initWithCapacity:count];
        for (int i = 0; i < count; ++i) {
            [set addObject:@(property_getName(properties[i]))];
        }
        free(properties);
    });
    return set;
}

これで、どのプロパティが再描画を引き起こす必要があるかを見つけるために Core Animation が使用するメソッドを書くことができます。

+ (BOOL)needsDisplayForKey:(NSString *)key {
    return [[self customPropertyKeys] containsObject:key] || [super needsDisplayForKey:key];
}

また、Core Animation がアニメーションのすべてのフレームでレイヤーのコピーを作成することもわかりました。Core Animation がコピーを作成するときに、これらすべてのプロパティを確実にコピーする必要があります。

- (id)initWithLayer:(id)layer {
    if (self = [super initWithLayer:layer]) {
        for (NSString *key in [self.class customPropertyKeys]) {
            [self setValue:[layer valueForKey:key] forKey:key];
        }
    }
    return self;
}

また、レイヤーの境界が変更された場合に再描画する必要があることを Core Animation に伝える必要があります。

- (BOOL)needsDisplayOnBoundsChange {
    return YES;
}

最後に、矢印の描画の核心に到達できます。まず、グラフィック コンテキストの原点をレイヤーの境界の中心に変更します。次に、矢印の輪郭を描くパスを作成します (原点が中心になります)。最後に、必要に応じてパスを塗りつぶしたり、ストロークしたりします。

- (void)drawInContext:(CGContextRef)gc {
    [self moveOriginToCenterInContext:gc];
    [self addArrowToPathInContext:gc];
    [self drawPathOfContext:gc];
}

原点を境界の中心に移動するのは簡単です:

- (void)moveOriginToCenterInContext:(CGContextRef)gc {
    CGRect bounds = self.bounds;
    CGContextTranslateCTM(gc, CGRectGetMidX(bounds), CGRectGetMidY(bounds));
}

矢印パスの構築は簡単ではありません。まず、尾が始まる半径位置、尾が終わり、矢じりが始まる半径位置、および矢じりの先端の半径位置を取得する必要があります。ヘルパー メソッドを使用して、これら 3 つの半径位置を計算します。

- (void)addArrowToPathInContext:(CGContextRef)gc {
    CGFloat startRadians;
    CGFloat headRadians;
    CGFloat tipRadians;
    [self getStartRadians:&startRadians headRadians:&headRadians tipRadians:&tipRadians];

次に、矢印の内側と外側の円弧の半径と先端の半径を計算する必要があります。

    CGFloat thickness = self.thickness;

    CGFloat outerRadius = self.bounds.size.width / 2;
    CGFloat tipRadius = outerRadius - thickness / 2;
    CGFloat innerRadius = outerRadius - thickness;

また、外側の円弧を時計回りまたは反時計回りのどちらで描画しているかを知る必要があります。

    BOOL outerArcIsClockwise = tipRadians > startRadians;

内側の円弧は反対方向に描画されます。

最後に、パスを構築できます。矢印の先端に移動し、2 つの円弧を追加します。このCGPathAddArc呼び出しは、パスの現在のポイントから円弧の始点までの直線を自動的に追加するため、自分で直線を追加する必要はありません。

    CGContextMoveToPoint(gc, tipRadius * cosf(tipRadians), tipRadius * sinf(tipRadians));
    CGContextAddArc(gc, 0, 0, outerRadius, headRadians, startRadians, outerArcIsClockwise);
    CGContextAddArc(gc, 0, 0, innerRadius, startRadians, headRadians, !outerArcIsClockwise);
    CGContextClosePath(gc);
}

それでは、これら 3 つの半径位置を計算する方法を考えてみましょう。これは些細なことですが、頭の長さが全体の長さよりも大きい場合に、頭の長さを全体の長さにクリップすることによって優雅にしたい場合を除きます。また、矢印を反対方向に描画するために、全長を負にしたいと考えています。開始位置、全長、ヘッドの長さをピックアップします。頭の長さを全体の長さよりも大きくならないようにクリップするヘルパーを使用します。

- (void)getStartRadians:(CGFloat *)startRadiansOut headRadians:(CGFloat *)headRadiansOut tipRadians:(CGFloat *)tipRadiansOut {
    *startRadiansOut = self.startRadians;
    CGFloat lengthRadians = self.lengthRadians;
    CGFloat headLengthRadians = [self clippedHeadLengthRadians];

次に、尾部が矢じりと交わる半径方向の位置を計算します。頭の長さを切り取った場合は、開始位置を正確に計算できるように、慎重に行います。これは、2 つの位置で呼び出したときにCGPathAddArc、浮動小数点の丸めによって予期しない円弧が追加されないようにするために重要です。

    // Compute headRadians carefully so it is exactly equal to startRadians if the head length was clipped.
    *headRadiansOut = *startRadiansOut + (lengthRadians - headLengthRadians);

Finally we compute the radial position of the tip of the arrowhead:

    *tipRadiansOut = *startRadiansOut + lengthRadians;
}

We need to write the helper that clips the head length. It also needs to ensure that the head length has the same sign as the overall length, so the computations above work correctly:

- (CGFloat)clippedHeadLengthRadians {
    CGFloat lengthRadians = self.lengthRadians;
    CGFloat headLengthRadians = copysignf(self.headLengthRadians, lengthRadians);

    if (fabsf(headLengthRadians) > fabsf(lengthRadians)) {
        headLengthRadians = lengthRadians;
    }
    return headLengthRadians;
}

To draw the path in the graphics context, we need to set the filling and stroking parameters of the context based on our properties, and then call CGContextDrawPath:

- (void)drawPathOfContext:(CGContextRef)gc {
    CGPathDrawingMode mode = 0;
    [self setFillPropertiesOfContext:gc andUpdateMode:&mode];
    [self setStrokePropertiesOfContext:gc andUpdateMode:&mode];

    CGContextDrawPath(gc, mode);
}

We fill the path if we were given a fill color:

- (void)setFillPropertiesOfContext:(CGContextRef)gc andUpdateMode:(CGPathDrawingMode *)modeInOut {
    UIColor *fillColor = self.fillColor;
    if (fillColor) {
        *modeInOut |= kCGPathFill;
        CGContextSetFillColorWithColor(gc, fillColor.CGColor);
    }
}

We stroke the path if we were given a stroke color and a line width:

- (void)setStrokePropertiesOfContext:(CGContextRef)gc andUpdateMode:(CGPathDrawingMode *)modeInOut {
    UIColor *strokeColor = self.strokeColor;
    CGFloat lineWidth = self.lineWidth;
    if (strokeColor && lineWidth > 0) {
        *modeInOut |= kCGPathStroke;
        CGContextSetStrokeColorWithColor(gc, strokeColor.CGColor);
        CGContextSetLineWidth(gc, lineWidth);
        CGContextSetLineJoin(gc, self.lineJoin);
    }
}

The end!

@end

So now we can go back to the view controller and use an ArrowLayer as the image view's mask:

- (void)setUpMask {
    arrowLayer = [ArrowLayer layer];
    arrowLayer.frame = imageView.bounds;
    arrowLayer.thickness = 60;
    arrowLayer.startRadians = -M_PI_2;
    arrowLayer.lengthRadians = 0;
    arrowLayer.headLengthRadians = M_PI_2 / 8;
    arrowLayer.fillColor = [UIColor whiteColor];
    imageView.layer.mask = arrowLayer;
}

And we can just animate the lengthRadians property from 0 to 2 π:

- (IBAction)goButtonWasTapped:(UIButton *)goButton {
    goButton.hidden = YES;
    [CATransaction begin]; {
        [CATransaction setAnimationDuration:2];
        [CATransaction setCompletionBlock:^{
            goButton.hidden = NO;
        }];

        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"lengthRadians"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.autoreverses = YES;
        animation.fromValue = @0.0f;
        animation.toValue = @((CGFloat)(2.0f * M_PI));
        [arrowLayer addAnimation:animation forKey:animation.keyPath];
    } [CATransaction commit];
}

and we get a glitch-free animation:

不具合のない矢印アニメーション

Core Animation インストゥルメントを使用して、iOS 6.0.1 を実行している iPhone 4S でこれをプロファイリングしました。毎秒 40 ~ 50 フレームを取得するようです。あなたのマイレージは異なる場合があります。プロパティ (iOS 6の新機能) をオンにしてみましたdrawsAsynchronouslyが、違いはありませんでした。

簡単にコピーできるように、この回答のコードを要点としてアップロードしました。

于 2012-11-27T06:45:11.877 に答える
4

アップデート

グリッチのないソリューションについては、私の他の回答を参照してください。

オリジナル

これは楽しい小さな問題です。Core Animation だけで完全に解決できるとは思いませんが、かなりうまくいくと思います。

ビューのレイアウト時にマスクを設定する必要があるため、画像ビューが最初に表示されるとき、またはサイズが変更されたときにのみ行う必要があります。それでは、次から実行してみましょうviewDidLayoutSubviews:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    [self setUpMask];
}

- (void)setUpMask {
    arrowLayer = [self arrowLayerWithFrame:imageView.bounds];
    imageView.layer.mask = arrowLayer;
}

arrowLayerはインスタンス変数なので、レイヤーをアニメーション化できます。

矢印の形をしたレイヤーを実際に作成するには、いくつかの定数が必要です。

static CGFloat const kThickness = 60.0f;
static CGFloat const kTipRadians = M_PI_2 / 8;
static CGFloat const kStartRadians = -M_PI_2;

static CGFloat const kEndRadians = kStartRadians + 2 * M_PI;
static CGFloat const kTipStartRadians = kEndRadians - kTipRadians;

これでレイヤーを作成できます。「矢印型」のライン エンド キャップがないため、先のとがった先端を含むパス全体の輪郭を描くパスを作成する必要があります。

- (CAShapeLayer *)arrowLayerWithFrame:(CGRect)frame {
    CGRect bounds = (CGRect){ CGPointZero, frame.size };
    CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
    CGFloat outerRadius = bounds.size.width / 2;
    CGFloat innerRadius = outerRadius - kThickness;
    CGFloat pointRadius = outerRadius - kThickness / 2;

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path addArcWithCenter:center radius:outerRadius startAngle:kStartRadians endAngle:kTipStartRadians clockwise:YES];
    [path addLineToPoint:CGPointMake(center.x + pointRadius * cosf(kEndRadians), center.y + pointRadius * sinf(kEndRadians))];
    [path addArcWithCenter:center radius:innerRadius startAngle:kTipStartRadians endAngle:kStartRadians clockwise:NO];
    [path closePath];

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.frame = frame;
    layer.path = path.CGPath;
    layer.fillColor = [UIColor whiteColor].CGColor;
    layer.strokeColor = nil;
    return layer;
}

これをすべて行うと、次のようになります。

フルアロー

ここで、矢印を一周させたいので、回転アニメーションをマスクに適用します。

- (IBAction)goButtonWasTapped:(UIButton *)goButton {
    goButton.enabled = NO;
    [CATransaction begin]; {
        [CATransaction setAnimationDuration:2];
        [CATransaction setCompletionBlock:^{
            goButton.enabled = YES;
        }];

        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.autoreverses = YES;
        animation.fromValue = 0;
        animation.toValue = @(2 * M_PI);
        [arrowLayer addAnimation:animation forKey:animation.keyPath];

    } [CATransaction commit];
}

[Go] ボタンをタップすると、次のようになります。

クリップされていない矢印の回転

もちろん、そうではありません。矢印の尾を切り取る必要があります。そのためには、マスクにマスクを適用する必要があります。直接適用することはできません(試しました)。代わりに、画像ビューのマスクとして機能する追加のレイヤーが必要です。階層は次のようになります。

Image view layer
  Mask layer (just a generic `CALayer` set as the image view layer's mask)
      Arrow layer (a `CAShapeLayer` as a regular sublayer of the mask layer)
          Ring layer (a `CAShapeLayer` set as the mask of the arrow layer)

新しいリング レイヤーは、最初にマスクを描画しようとしたときと同じように、単一のストローク ARC セグメントになります。を書き直して階層を設定しますsetUpMask

- (void)setUpMask {
    CALayer *layer = [CALayer layer];
    layer.frame = imageView.bounds;
    imageView.layer.mask = layer;
    arrowLayer = [self arrowLayerWithFrame:layer.bounds];
    [layer addSublayer:arrowLayer];
    ringLayer = [self ringLayerWithFrame:arrowLayer.bounds];
    arrowLayer.mask = ringLayer;
    return;
}

これもアニメーション化する必要があるため、もう 1 つの ivarringLayerがあります。arrowLayerWithFrame:メソッドは変更されていません。リングレイヤーを作成する方法は次のとおりです。

- (CAShapeLayer *)ringLayerWithFrame:(CGRect)frame {
    CGRect bounds = (CGRect){ CGPointZero, frame.size };
    CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
    CGFloat radius = (bounds.size.width - kThickness) / 2;

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.frame = frame;
    layer.path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:kStartRadians endAngle:kEndRadians clockwise:YES].CGPath;
    layer.fillColor = nil;
    layer.strokeColor = [UIColor whiteColor].CGColor;
    layer.lineWidth = kThickness + 2; // +2 to avoid extra anti-aliasing
    layer.strokeStart = 1;
    return layer;
}

strokeStartを 0に設定するのではなく、 を 1 に設定していることに注意してくださいstrokeEnd。ストロークの終点は矢印の先端にあり、常に先端を表示したいので、そのままにしておきます。

最後に、(矢印レイヤーの回転のアニメーション化に加えて)goButtonWasTappedリング レイヤーのアニメーション化を書き直します。strokeStart

- (IBAction)goButtonWasTapped:(UIButton *)goButton {
    goButton.hidden = YES;
    [CATransaction begin]; {
        [CATransaction setAnimationDuration:2];
        [CATransaction setCompletionBlock:^{
            goButton.hidden = NO;
        }];

        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.autoreverses = YES;
        animation.fromValue = 0;
        animation.toValue = @(2 * M_PI);
        [arrowLayer addAnimation:animation forKey:animation.keyPath];

        animation.keyPath = @"strokeStart";
        animation.fromValue = @1;
        animation.toValue = @0;
        [ringLayer addAnimation:animation forKey:animation.keyPath];

    } [CATransaction commit];
}

最終結果は次のようになります。

クリップされた矢印の回転

まだ完璧ではありません。テールに小さな揺れがあり、そこに青いピクセルの列が表示されることがあります. 先端に白い線のささやきが入ることもあります。これは、Core Animation がアークを内部的に (3 次ベジェ スプラインとして) 表現する方法によるものだと思います。のパスに沿った距離を完全に測定することはできないためstrokeStart、概算します。概算は、一部のピクセルが漏れるほどずれている場合があります。kEndRadiansこれに変更することで、ヒントの問題を修正できます。

static CGFloat const kEndRadians = kStartRadians + 2 * M_PI - 0.01;

strokeStartまた、アニメーション エンドポイントを微調整することで、尾から青いピクセルを取り除くことができます。

        animation.keyPath = @"strokeStart";
        animation.fromValue = @1.01f;
        animation.toValue = @0.01f;
        [ringLayer addAnimation:animation forKey:animation.keyPath];

しかし、まだ尻尾が揺れているのがわかります。

クリップされた矢印を調整して回転させる

それ以上のことをしたい場合は、すべてのフレームで実際に矢印の形を再現してみることができます。それがどれほど速くなるかはわかりません。

于 2012-11-26T23:22:57.317 に答える
0

残念ながら、パス描画には、説明したような尖ったライン キャップを持つオプションはありません (必要なものではなく、CAShapeLayerのプロパティを使用してオプションを使用できます)。lineCap

ストロークの幅に頼るのではなく、パスの境界を自分で描いて塗りつぶす必要があります。これは、3 つの線分と 2 つの円弧を意味します。これは扱いやすいはずですが、試みたほど簡単ではありません。

于 2012-11-26T21:49:28.683 に答える