核心animation中的圆形箭头蒙版的长度的animation

我已经使用CAShapeLayer和蒙版创build了一个圆形的animation。 这是我的代码:

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

这是我的背景图片: 在这里输入图像说明

这是我运行animation时的样子: 在这里输入图像说明

但是,我想要的箭头缺less如何添加这个? 在这里输入图像说明

由于我的其他答案(animation两个级别的面具)有一些graphics故障,我决定尝试重绘每一个animation帧的path。 所以首先让我们写一个类似CAShapeLayerCALayer子类,只是画一个箭头。 我最初尝试使它成为CAShapeLayer一个子类,但是我无法让Core Animation正确地设置它的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 

startRadians属性是尾部尾部的位置(弧度)。 长度lengthRadians是从尾部到箭头尖的长度(弧度)。 headLengthRadians是箭头的长度(以弧度表示)。

我们还重现了CAShapeLayer一些属性。 我们不需要lineCap属性,因为我们总是绘制一个封闭的path。

那么,我们该如何实现这个疯狂的事情呢? 恰巧, 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将在每个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; } 

最后,我们可以看到绘制箭头的本质。 首先,我们将graphics上下文的原点更改为图层边界的中心。 然后,我们将构build概述箭头的path(现在以原点为中心)。 最后,我们会适当地填充和/或描边path。

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

构build箭头path不是微不足道的。 首先,我们需要得到尾部开始的径向位置,尾部和箭头开始的径向位置以及箭头顶端的径向位置。 我们将使用一个辅助方法来计算这三个径向位​​置:

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

内弧将以相反的方向绘制。

最后,我们可以构buildpath。 我们移动到箭头的尖端,然后添加两个弧。 CGPathAddArc调用会自动从path的当前点向弧的起点添加一条直线,所以我们不需要自己添加任何直线:

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

现在让我们弄清楚如何计算这三个径向位​​置。 这是微不足道的,除非我们希望在头部长度大于总体长度时变得优美,通过将头部长度剪切成整体长度。 我们也想让整体长度为负,画出相反方向的箭头。 我们将从开始位置,总长度和头部长度开始。 我们将使用一个辅助工具来剪裁头部长度,使其不超过整个长度:

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

接下来我们计算尾部与箭头相交的径向位置。 我们小心翼翼地这样做,所以如果我们缩短了头部的长度,我们就可以准确计算起始位置。 这很重要,所以当我们用两个位置调用CGPathAddArc时,由于浮点舍入,它不会添加意外的弧。

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

最后我们计算箭头尖端的径向位置:

  *tipRadiansOut = *startRadiansOut + lengthRadians; } 

我们需要编写剪辑头部长度的助手。 还需要确保头部长度与总长度具有相同的符号,所以上面的计算正确地工作:

 - (CGFloat)clippedHeadLengthRadians { CGFloat lengthRadians = self.lengthRadians; CGFloat headLengthRadians = copysignf(self.headLengthRadians, lengthRadians); if (fabsf(headLengthRadians) > fabsf(lengthRadians)) { headLengthRadians = lengthRadians; } return headLengthRadians; } 

为了在graphics上下文中绘制path,我们需要根据属性设置上下文的填充和描边参数,然后调用CGContextDrawPath

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

如果给我们填充颜色,我们填写path:

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

如果给我们一个笔触的颜色和一个线条宽度,我们就会沿着这条path行进

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

结束!

 @end 

所以现在我们可以回到视图控制器并使用一个ArrowLayer作为图像视图的掩码:

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

而且我们可以制作从0到2π的lengthRadians属性的animation:

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

我们得到一个无故障的animation:

箭头动画没有毛刺

我使用Core Animation工具在运行iOS 6.0.1的iPhone 4S上对此进行了描述。 它似乎每秒获得40-50帧。 你的旅费可能会改变。 我尝试打开drawsAsynchronously属性(iOS 6中的新function),但没有什么区别。

我已经上传了这个答案的代码作为一个简单的复制的要点 。

UPDATE

看到我的其他答案 ,没有毛刺的解决scheme。

原版的

这是一个有趣的小问题。 我不认为我们可以用Core Animation完全解决它,但我们可以做得很好。

在布局视图时,我们应该设置遮罩,所以我们只需要在图像视图第一次出现或者当它改变大小的时候这样做。 所以让我们从viewDidLayoutSubviews

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

在这里, arrowLayer是一个实例variables,所以我可以animation层。

要实际创build箭头形图层,我需要一些常量:

 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; 

现在我可以创build图层了。 既然没有“箭头形”的线头帽,我必须制定一个概括整个path的path,包括尖尖的提示:

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

如果我们这样做,它看起来像这样:

充分的箭头

现在,我们希望箭头四处走动,所以我们将旋转animation应用于蒙版:

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

当我们点击Gobutton时,它看起来像这样:

旋转未剪切的箭头

那当然是不对的 我们需要剪辑箭头尾巴。 要做到这一点,我们需要在面具上涂抹面膜。 我们不能直接应用(我试过)。 相反,我们需要一个额外的图层来充当图像视图的蒙版。 层次结构如下所示:

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

我们现在有另一个伊娃, ringLayer ,因为我们也需要animation。 arrowLayerWithFrame:方法不变。 以下是我们如何创build环形图层:

 - (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设置为1,而不是将strokeEnd设置为0.笔划结束位于箭头的末端,我们总是希望提示可见,所以我们将其单独放置。

最后,我们重写goButtonWasTapped来为环形图层的strokeStart设置animation(除了animation箭头图层的旋转):

 - (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在内部表示圆弧的方式(如三次Bezier样条曲线)。 它不能完美地测量strokeStart的path上的距离,所以它近似,有时候近似是足够泄漏一些像素。 您可以通过更改kEndRadians来解决提示问题:

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

您可以通过调整strokeStartanimation终结点来消除尾部的蓝色像素:

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

但你仍然会看到尾巴摆动:

旋转剪辑箭头与调整

如果你想做的比这更好,你可以尝试在每一帧重新创build箭头形状。 我不知道会有多快。

不幸的是,在path绘制中没有任何选项可以像你描述的那样有一个尖锐的线条帽(选项可以使用CAShapeLayerlineCap属性,而不是你需要的)。

您将不得不自己绘制path边界并填充它,而不是依赖笔划的宽度。 这意味着3行2弧,这应该是可以pipe理的,尽pipe不像你试图做的那样直截了当。