Since my other answer (animating two levels of masks) has some graphics glitches, I decided to try redrawing the path on every frame of animation. So first let's write a CALayer subclass that's like CAShapeLayer, but just draws an arrow. I originally tried making it a subclass of CAShapeLayer, but I could not get Core Animation to properly animate it.

Anyway, here's the interface we're going to implement:

@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.

We also reproduce some of the properties of CAShapeLayer. We don't need the lineCap property because we always draw a closed path.

So, how do we implement this crazy thing? As it happens, CALayer will take care of storing any old property you want to define on a subclass. So first, we just tell the compiler not to worry about synthesizing the properties:

@implementation ArrowLayer

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

But we need to tell Core Animation that we need to redraw the layer if any of those properties change. To do that, we need a list of the property names. We'll use the Objective-C runtime to get a list so we don't have to retype the property names. We need to #import <objc/runtime.h> at the top of the file, and then we can get the list like this:

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

Now we can write the method that Core Animation uses to find out which properties need to cause a redraw:

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

It also turns out that Core Animation will make a copy of our layer in every frame of animation. We need to make sure we copy over all of these properties when Core Animation makes the copy:

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

We also need to tell Core Animation that we need to redraw if the layer's bounds change:

- (BOOL)needsDisplayOnBoundsChange {
return YES;
}

Finally, we can get to the nitty-gritty of drawing the arrow. First, we'll change the graphic context's origin to be at the center of the layer's bounds. Then we'll construct the path outlining the arrow (which is now centered at the origin). Finally, we'll fill and/or stroke the path as appropriate.

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

Moving the origin to the center of our bounds is trivial:

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

Constructing the arrow path is not trivial. First, we need to get the radial position at which the tail starts, the radial position at which the tail ends and the arrowhead starts, and the radial position of the tip of the arrowhead. We'll use a helper method to compute those three radial positions:

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

Then we need to figure out the radius of the inside and outside arcs of the arrow, and the radius of the tip:

    CGFloat thickness = self.thickness;

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

We also need to know whether we're drawing the outer arc in a clockwise or counterclockwise direction:

    BOOL outerArcIsClockwise = tipRadians > startRadians;

The inner arc will be drawn in the opposite direction.

Finally, we can construct the path. We move to the tip of the arrowhead, then add the two arcs. The CGPathAddArc call automatically adds a straight line from the path's current point to the starting point of the arc, so we don't need to add any straight lines ourselves:

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

Now let's figure out how to compute those three radial positions. This would be trivial, except we want to be graceful when the head length is larger than the overall length, by clipping the head length to the overall length. We also want to let the overall length be negative to draw the arrow in the opposite direction. We'll start by picking up the start position, the overall length, and the head length. We'll use a helper that clips the head length to be no larger than the overall length:

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

Next we compute the radial position where the tail meets the arrowhead. We do so carefully, so that if we clipped the head length, we compute exactly the start position. This is important so that when we call CGPathAddArc with the two positions, it doesn't add an unexpected arc due to floating-point rounding.

    // 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:

I profiled this on my iPhone 4S running iOS 6.0.1 using the Core Animation instrument. It seems to get 40-50 frames per second. Your mileage may vary. I tried turning on the drawsAsynchronously property (new in iOS 6) but it didn't make a difference.

I've uploaded the code in this answer as a gist for easy copying.


A CALayer subclass that draws a very simple arrow

 #import <QuartzCore/QuartzCore.h>

 @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

ArrowLayer.m

#import "ArrowLayer.h"
#import <objc/runtime.h> @implementation ArrowLayer @dynamic thickness;
@dynamic startRadians;
@dynamic lengthRadians;
@dynamic headLengthRadians;
@dynamic fillColor;
@dynamic strokeColor;
@dynamic lineWidth;
@dynamic lineJoin; + (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 = ; i < count; ++i) {
[set addObject:@(property_getName(properties[i]))];
}
free(properties);
});
return set;
} + (BOOL)needsDisplayForKey:(NSString *)key {
return [[self customPropertyKeys] containsObject:key] || [super needsDisplayForKey:key];
} - (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;
} - (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));
} - (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 / ;
CGFloat tipRadius = outerRadius - thickness / ;
CGFloat innerRadius = outerRadius - thickness; BOOL outerArcIsClockwise = tipRadians > startRadians; CGContextMoveToPoint(gc, tipRadius * cosf(tipRadians), tipRadius * sinf(tipRadians));
CGContextAddArc(gc, , , outerRadius, headRadians, startRadians, outerArcIsClockwise);
CGContextAddArc(gc, , , 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]; // 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;
} - (void)drawPathOfContext:(CGContextRef)gc {
CGPathDrawingMode mode = ;
[self setFillPropertiesOfContext:gc andUpdateMode:&mode];
[self setStrokePropertiesOfContext:gc andUpdateMode:&mode]; CGContextDrawPath(gc, mode);
} - (void)setFillPropertiesOfContext:(CGContextRef)gc andUpdateMode:(CGPathDrawingMode *)modeInOut {
UIColor *fillColor = self.fillColor;
if (fillColor) {
*modeInOut |= kCGPathFill;
CGContextSetFillColorWithColor(gc, fillColor.CGColor);
}
} - (void)setStrokePropertiesOfContext:(CGContextRef)gc andUpdateMode:(CGPathDrawingMode *)modeInOut {
UIColor *strokeColor = self.strokeColor;
CGFloat lineWidth = self.lineWidth;
if (strokeColor && lineWidth > ) {
*modeInOut |= kCGPathStroke;
CGContextSetStrokeColorWithColor(gc, strokeColor.CGColor);
CGContextSetLineWidth(gc, lineWidth);
CGContextSetLineJoin(gc, self.lineJoin);
}
} @end

See http://stackoverflow.com/a/13578767/77567 for an explanation of this code.

ArrowLayer : A coustom layer animation的更多相关文章

  1. arcmap Command

    The information in this document is useful if you are trying to programmatically find a built-in com ...

  2. 【原】iOS学习44之动画

    1. 简单动画 1> UIImageView GIF 动画 GIF图的原理是:获取图片,存储在图片数组中,按照图片数组的顺序将图片以一定的速度播放 UIImageView *showGifima ...

  3. Animated progress view with CAGradientLayer(带翻译)<待更新>

    原文网址:使用CAGradientLayer的动画精度条View Modern software design is getting flatter and thinner all the time. ...

  4. Unity3D脚本中文系列教程(四)

    http://dong2008hong.blog.163.com/blog/static/4696882720140302451146/ Unity3D脚本中文系列教程(三) 送到动画事件. ◆ va ...

  5. UI进阶 动画

    前言:所谓动画,即应用界面上展示的各种过渡效果,不过其实没有动画并不影响我们产品的功能实现 一.动画 1.动画可以达到的效果 传达状态 提高用户对直接操作的感知 帮助用户可视化操作的结果 2.使用动画 ...

  6. CAShapeLayer和CAGradientLayer

    两个动画效果来了解一下CALayer的两个重要的subClass,CAGradientLayer和CAShapeLayer. 微视录制视频的时候那个进度效果和Spark相机类似,但是个人还是比较喜欢S ...

  7. Cocos2d-X使用CCAnimation创建动画

    动画在游戏中是很常见的 程序1:创建一个简单的动画 首先须要在project文件夹下的Resource文件夹中放一张有各种不同动作的图片 在程序中加入以下的代码 #include "Anim ...

  8. 关于Unity中旧版动画系统的使用

    Unity在5.X以后,有一个旧版的动画系统和新版的动画系统. 新版的动画系统是使用Unity动画编辑器来调的,调动画和控制动画 旧版的动画系统是用其他的第三方软件调好后导出到一个FBX文件里面,就是 ...

  9. [翻译] LLSimpleCamera

    LLSimpleCamera https://github.com/omergul123/LLSimpleCamera LLSimpleCamera is a library for creating ...

随机推荐

  1. 动态获取R.drawable.xx资源

    String imageName = "index_fragmen"+getColor();final int resId = context.getResources().get ...

  2. 编译GCC4.8.2

        公司的机器上默认是GCC4.4.6,为了完整的体验C++11,可以自己编译一个GCC4.8.2出来(自带了更高版本的glibc). 以下是备忘. 1.下载GCC源码以及依赖库 gmp ftp: ...

  3. EF6 CodeFirst+Repository+Ninject+MVC4+EasyUI实践(四)

    前言 这一篇,我们终于到了讲解Entity Framework CodeFirst 的时刻了,首先创建实体对象模型,然后会通过配置Fluent API的方式来对实体对象模型进行完整的数据库映射操作. ...

  4. Android ORM应用开发框架KJFrameForAndroid使用详解

    本文将为大家介绍一款Android ORM应用开发框架KJFrameForAndroid,很多时候我们也叫它KJLibrary. KJFrameForAndroid简介 KJFrameForAndro ...

  5. C++与C的关系

     C是结构化和模块化的语言,是基于过程的.        C++是面向对象的程序设计语言.        C++是C的超集,对C的功能做了扩充,增加了面向对象的机制.        C++包含头文件的 ...

  6. Java集合类学习笔记(Set集合)

    Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入. HashSet类的特点: 不能保证元素的排列顺序,顺 ...

  7. Ubuntu安装node

    #!/bin/bash echo "添加环境变量需要root权限,如无root权限,则不添加环境变量" echo "输入Node下载地址(目前仅支持Node官方网站上Li ...

  8. 如何下载youtube上面的视频

    youtube做为全球最大的视频共享网站,其视频数量难以计数. 那么你是不是从上面发现了你中意MV,或者一些别的视频?但是你却为无法下载这些视频而苦恼? http://vixy.net/flv_con ...

  9. js中(function(){…})()立即执行函数写法理解

    文章摘自https://my.oschina.net/u/2331760/blog/468672?p={{currentPage+1}} 摘要: javascript和其他编程语言相比比较随意,所以j ...

  10. 我收录整理的优秀OC技术类文章

        自定义导航按钮UIBarButtonItem   关于导航栏的六个小技巧   ios开发的一些小技巧篇一 制作一个可以滑动操作的 Table View Cell - IOS - 伯乐在线 一个 ...