Created
February 23, 2014 18:08
-
-
Save toriaezunama/9174945 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
///// View controller ///// | |
#include "CGCustomPropertyLayer.h" | |
- (void)viewDidLoad | |
{ | |
[super viewDidLoad]; | |
_layer = [CGCustomPropertyLayer new]; | |
[self.customLayerView.layer addSublayer:_layer]; | |
_layer.bounds = self.customLayerView.bounds; | |
_layer.position = CGPointMake( CGRectGetWidth(_layer.bounds)/2, CGRectGetHeight(_layer.bounds)/2 ); | |
[_layer setNeedsDisplay]; | |
} | |
- (void)viewDidAppear:(BOOL)animated { | |
[super viewDidAppear:animated]; | |
// Animation | |
// Explicit | |
/* | |
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:kKeyPercentComplete]; | |
anim.duration = 5; | |
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; | |
anim.autoreverses = YES; | |
anim.repeatCount = MAXFLOAT; | |
anim.toValue = @(1.f); | |
[_layer addAnimation:anim forKey:kKeyPercentComplete]; | |
/*/ | |
// Implicit | |
_layer.percentComplete = 1; // can also use [_layer setValue:@(1) forKey:kKeyPercentComplete]; | |
//*/ | |
} | |
///// CGCustomPropertyLayer.h ///// | |
#import <QuartzCore/QuartzCore.h> | |
@interface CGCustomPropertyLayer : CALayer | |
#define kKeyPercentComplete @"percentComplete" | |
@property (assign, nonatomic) CGFloat percentComplete; | |
@property (assign, nonatomic) BOOL nonDynamicProperty; | |
@end | |
///// CGCustomPropertyLayer.m ///// | |
@implementation CGCustomPropertyLayer { | |
NSTimer *_timer; // Used to show that percentProperty's value is still interpolated even when needsDisplayForKey: returns NO for key kKeyPercentComplete | |
} | |
////////////// Implicit animation | |
/* | |
Firstly don’t @synthesize the animatable properties and instead mark them as @dynamic. | |
This is required because Core Animation does some magic under the hood to track changes to these properties and call appropriate methods on your layer. | |
Without this actionForKey: isn't called which means no animation is created for the property change. | |
*/ | |
@dynamic percentComplete; | |
- (id)init { | |
self = [super init]; | |
if (self) { | |
[self setup]; | |
return self; | |
} | |
return nil; | |
} | |
// @dynamic properties are automatically copied over during initWithLayer: | |
// BUT non dynamic i.e. synthesized properties and instance properties aren't so in order | |
// to get these copied we override initWithLayer: | |
// This method gets called for each frame of animation. | |
// Core Animation makes a copy of the presentationLayer for each frame of the animation. | |
// By overriding this method we make sure our custom properties are correctly transferred to the copied-layer. | |
// This initializer is used to create shadow copies of layers, for example, for the presentationLayer method. Using this method in any other situation will produce undefined behavior. For example, do not use this method to initialize a new layer with an existing layer’s content. | |
//If you are implementing a custom layer subclass, you can override this method and use it to copy the values of instance variables into the new object. Subclasses should always invoke the superclass implementation. | |
//This method is the designated initializer for layer objects in the presentation layer. | |
- (id)initWithLayer:(id)layer | |
{ | |
if (self = [super initWithLayer:layer]) { | |
if ([layer isKindOfClass:[CGCustomPropertyLayer class]]) { | |
CGCustomPropertyLayer *cpLayer = layer; | |
// Copy across ... | |
self.nonDynamicProperty = cpLayer.nonDynamicProperty; | |
} | |
} | |
return self; | |
} | |
- (void)setup { | |
// Because _percentComplete isn't declared (because it's @dynamic) we have to use the property setter to set an initial value which will create an implicit animation. | |
// There disable implicit animation for this first value | |
[CATransaction begin]; | |
[CATransaction setDisableActions:YES]; | |
self.percentComplete = 0.0; | |
[CATransaction commit]; | |
_timer = [NSTimer scheduledTimerWithTimeInterval:0.25 | |
target:self | |
selector:@selector(timerCallback:) | |
userInfo:nil | |
repeats:YES]; | |
[self addObserver:self | |
forKeyPath:kKeyPercentComplete | |
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | |
context:nil]; | |
self.nonDynamicProperty = true; | |
} | |
// We also need to override needsDisplayForKey: to tell Core Animation that changes to our custom properties will require a redraw | |
// Called by [super init] | |
// Returning NO means drawInContext: won't be called. But the value it's self is still animated and can be accessed by layer.presentationLayer.percentComplete | |
+ (BOOL)needsDisplayForKey:(NSString*)key { | |
if ([key isEqualToString:kKeyPercentComplete]) { | |
return YES; // Set to NO to just animate the property | |
} else { | |
return [super needsDisplayForKey:key]; | |
} | |
} | |
// Can't use defaultActionForKey: because there is no self pointer and hence we don't have access to the layer and it's properties | |
// Therefore we use actionForKey: | |
// Seems (*) to be called during the setter for percentComplete BEFORE the value is set so | |
// the value returned from self.percentComplete is still the OLD value | |
// (*) actionForKey: is called BEFORE observeValueForKeyPath: suggesting this to be the case | |
- (id<CAAction>)actionForKey:(NSString *)event | |
{ | |
if ([event isEqualToString:kKeyPercentComplete]) { | |
NSLog( @"actionForKey:%@ = %f %@", event, self.percentComplete, self ); | |
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event]; | |
anim.duration = 5; | |
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; | |
anim.autoreverses = YES; | |
anim.repeatCount = MAXFLOAT; | |
anim.fromValue = @(self.percentComplete); | |
return anim; | |
} else { | |
return [super actionForKey:event]; | |
} | |
} | |
- (void)observeValueForKeyPath:(NSString *)keyPath | |
ofObject:(id)object | |
change:(NSDictionary *)change | |
context:(void *)context | |
{ | |
if ([keyPath isEqual:kKeyPercentComplete]) { | |
NSInteger type = [change[ NSKeyValueChangeKindKey ] intValue ]; | |
// Value of observe object has changed | |
if( type == NSKeyValueChangeSetting ) { | |
// NSKeyValueChangeInsertion | |
// NSKeyValueChangeRemoval | |
// NSKeyValueChangeReplacement | |
// Do something | |
} | |
NSLog( @"Old: %@", change[ NSKeyValueChangeOldKey ] ); | |
NSLog( @"New: %@", change[ NSKeyValueChangeNewKey ] ); | |
CGCustomPropertyLayer *layer = object; | |
NSLog( @"PercentComplete: %f %@", layer.percentComplete, layer ); | |
} | |
} | |
// Show percentComplete's value animating | |
- (void)timerCallback:(NSTimer *)timer | |
{ | |
CGCustomPropertyLayer *layer = self.presentationLayer; | |
NSLog( @">>> %f", layer.percentComplete ); | |
} | |
// Use percent complete to animate the path | |
- (void)drawInContext:(CGContextRef)context { | |
//NSLog( @"%@ drawInContext:%@ %f", self, context, self.percentComplete ); | |
// Path set up | |
CGFloat outerRadius = MIN( CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds) ) /2; | |
CGFloat innerRadius = outerRadius * 0.5; | |
CGFloat size = self.percentComplete * 2 * M_PI; | |
CGPathRef path = [self segmentPathWithOuterRadius:outerRadius | |
innerRadius:innerRadius | |
angle:size]; | |
CGContextAddPath(context, path); | |
CGContextSetFillColorWithColor(context,[UIColor redColor].CGColor); | |
CGContextFillPath(context); | |
} | |
- (CGPathRef)segmentPathWithOuterRadius:(CGFloat)outer | |
innerRadius:(CGFloat)inner | |
angle:(CGFloat)size | |
{ | |
CGPoint centre = CGPointMake( CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds) /2); | |
UIBezierPath *path = [UIBezierPath bezierPath]; | |
[path removeAllPoints]; | |
[path moveToPoint:centre ]; | |
[path addArcWithCenter:centre // centre | |
radius:outer | |
// 0 is +ve x axis and the sweep is clockwise | |
startAngle:0 | |
endAngle:size | |
clockwise:YES]; | |
[path addArcWithCenter:centre | |
radius:inner | |
// 0 is +ve x axis and the sweep is clockwise | |
startAngle:size | |
endAngle:0 | |
clockwise:NO]; | |
return path.CGPath; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment